Repository: borgbackup/borg

Issue:
borg create: No archive specified but it is specified!
Hello,

When I run the following
```
[smith@host2] $ borg create -v --info --stats --exclude '*.gvfs*' --exclude '*.thumbnails*' \
--exclude '*.backup*' --exclude '*.ccache' --exclude '*.[Cc]ache' --exclude '*/[Cc]ache' \
--exclude '*/Downloads' --exclude '*/[Tr]ash*' --exclude '*~' \
host1:/data/borgbackups/smith::2016-12-05@11:28:34 /home/smith
```
I get this error:
```
borg create: error: argument ARCHIVE: "host1:/data/borgbackups/smith::2016-12-05@09:46:28": No archive specified
```

It used to work fine before I upgraded borgbackup to 1.0.8
```
[smith@host2] $ rpm -q borgbackup
borgbackup-1.0.8-2.fc25.x86_64
```

Fuji


Discussion:
Try smith@host1 instead of just host1.

I suspect the parser gets confused by you using @ inside the archive name.

Possibly a duplicate of #1929.

You said it worked before - what version did you use before 1.0.8?
Yes, it's working now with smith@host1:...
It is actually the same problem as [#1929](https://github.com/borgbackup/borg/issues/1929).
I think the previous version I used was 1.0.7.
OK, maybe this was unintentionally caused by some parsing regex change.

We'll have a look if we can improve that...

Resolving patch:
diff --git a/borg/helpers.py b/borg/helpers.py
--- a/borg/helpers.py
+++ b/borg/helpers.py
@@ -773,6 +773,17 @@ class Location:
     """
     proto = user = host = port = path = archive = None
 
+    # user must not contain "@", ":" or "/".
+    # Quoting adduser error message:
+    # "To avoid problems, the username should consist only of letters, digits,
+    # underscores, periods, at signs and dashes, and not start with a dash
+    # (as defined by IEEE Std 1003.1-2001)."
+    # We use "@" as separator between username and hostname, so we must
+    # disallow it within the pure username part.
+    optional_user_re = r"""
+        (?:(?P<user>[^@:/]+)@)?
+    """
+
     # path must not contain :: (it ends at :: or string end), but may contain single colons.
     # to avoid ambiguities with other regexes, it must also not start with ":".
     path_re = r"""
@@ -791,7 +802,7 @@ class Location:
     # regexes for misc. kinds of supported location specifiers:
     ssh_re = re.compile(r"""
         (?P<proto>ssh)://                                   # ssh://
-        (?:(?P<user>[^@]+)@)?                               # user@  (optional)
+        """ + optional_user_re + r"""                       # user@  (optional)
         (?P<host>[^:/]+)(?::(?P<port>\d+))?                 # host or host:port
         """ + path_re + optional_archive_re, re.VERBOSE)    # path or path::archive
 
@@ -802,7 +813,7 @@ class Location:
     # note: scp_re is also use for local pathes
     scp_re = re.compile(r"""
         (
-            (?:(?P<user>[^@]+)@)?                           # user@  (optional)
+            """ + optional_user_re + r"""                   # user@  (optional)
             (?P<host>[^:/]+):                               # host: (don't match / in host to disambiguate from file:)
         )?                                                  # user@host: part is optional
         """ + path_re + optional_archive_re, re.VERBOSE)    # path with optional archive
diff --git a/borg/testsuite/helpers.py b/borg/testsuite/helpers.py
--- a/borg/testsuite/helpers.py
+++ b/borg/testsuite/helpers.py
@@ -94,6 +94,13 @@ def test_with_colons(self, monkeypatch):
         assert repr(Location('/abs/path:with:colons')) == \
             "Location(proto='file', user=None, host=None, port=None, path='/abs/path:with:colons', archive=None)"
 
+    def test_user_parsing(self):
+        # see issue #1930
+        assert repr(Location('host:path::2016-12-31@23:59:59')) == \
+            "Location(proto='ssh', user=None, host='host', port=None, path='path', archive='2016-12-31@23:59:59')"
+        assert repr(Location('ssh://host/path::2016-12-31@23:59:59')) == \
+            "Location(proto='ssh', user=None, host='host', port=None, path='/path', archive='2016-12-31@23:59:59')"
+
     def test_underspecified(self, monkeypatch):
         monkeypatch.delenv('BORG_REPO', raising=False)
         with pytest.raises(ValueError):


Test patch: