Repository: borgbackup/borg

Issue:
`borg extract --stdout` with leading slashes fails silently
i recently had to restore my .zsh_history file. as many runnign processes might overwrite it, i went for extraction to --stdout (to redirect it to ~/.zsh_history-extracted; i might instead as well have used direct extraction in another directory had i read the changelog of 0.24 before).

while

```
borg extract prometheus:hephaistos.borg::chrysn-full-2015-09-17-10:20 home/chrysn/.zsh_history --stdout >! .zsh_history-borg-stdoutrestored
```

worked fine,

```
borg extract prometheus:hephaistos.borg::chrysn-full-2015-09-17-10:20 /home/chrysn/.zsh_history --stdout >! .zsh_history-borg-stdoutrestored-leadingslash
```

created an empty file without indicating any error. at least, it should give an error message when it doesn't find the file at all, but practically, it should either accept the leading slash or immediately err out. the issue is especially present in user directories, where one would be tempted to do `borg extract ... ~/.zsh_history`, which is expanded by the shell to the leading-slash form.

the borg version used that showed that behavior was 0.23.0, apologies if it's already fixed in later versions.



Discussion:
the issue title and description is a bit misleading.

your main issue seems to be that extracting a file that does not exist in the repository fails silently.
that does not depend on --stdout, it also happens in the same way without that.

about the other issue: I guess we can't fix the leading slash issue as creating an archive from a relative path works also and is also stored without a leading "/", so we can't know later if the "/" was there or not and whether we could just strip it or not.

Looking at it more generally: it is allowed to give multiple path arguments. So, to implement this, we would need to remember whether we have matched each of them once at least.

Even more generally, there might be also exclude patterns that do not match (but I think we should not complain about them not matching).


Resolving patch:
diff --git a/borg/archiver.py b/borg/archiver.py
--- a/borg/archiver.py
+++ b/borg/archiver.py
@@ -21,7 +21,7 @@
 from .cache import Cache
 from .key import key_creator
 from .helpers import Error, location_validator, format_time, format_file_size, \
-    format_file_mode, ExcludePattern, exclude_path, adjust_patterns, to_localtime, timestamp, \
+    format_file_mode, ExcludePattern, IncludePattern, exclude_path, adjust_patterns, to_localtime, timestamp, \
     get_cache_dir, get_keys_dir, format_timedelta, prune_within, prune_split, \
     Manifest, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \
     is_cachedir, bigint_to_int, ChunkerParams, CompressionSpec
@@ -286,6 +286,9 @@ def do_extract(self, args):
         if not args.dry_run:
             while dirs:
                 archive.extract_item(dirs.pop(-1))
+        for pattern in (patterns or []):
+            if isinstance(pattern, IncludePattern) and  pattern.match_count == 0:
+                self.print_error("Warning: Include pattern '%s' never matched.", pattern)
         return self.exit_code
 
     def do_rename(self, args):
diff --git a/borg/helpers.py b/borg/helpers.py
--- a/borg/helpers.py
+++ b/borg/helpers.py
@@ -242,6 +242,7 @@ def normalize_wrapper(self, path):
         # always have to enter an exact match
         return func
 
+
 class IncludePattern:
     """Literal files or directories listed on the command line
     for some operations (e.g. extract, but not create).
@@ -249,6 +250,9 @@ class IncludePattern:
     path match as well.  A trailing slash makes no difference.
     """
     def __init__(self, pattern):
+        self.pattern_orig = pattern
+        self.match_count = 0
+
         if sys.platform in ('darwin',):
             pattern = unicodedata.normalize("NFD", pattern)
 
@@ -256,17 +260,26 @@ def __init__(self, pattern):
 
     @normalized
     def match(self, path):
-        return (path+os.path.sep).startswith(self.pattern)
+        matches = (path+os.path.sep).startswith(self.pattern)
+        if matches:
+            self.match_count += 1
+        return matches
 
     def __repr__(self):
         return '%s(%s)' % (type(self), self.pattern)
 
+    def __str__(self):
+        return self.pattern_orig
+
 
 class ExcludePattern(IncludePattern):
     """Shell glob patterns to exclude.  A trailing slash means to
     exclude the contents of a directory, but not the directory itself.
     """
     def __init__(self, pattern):
+        self.pattern_orig = pattern
+        self.match_count = 0
+
         if pattern.endswith(os.path.sep):
             self.pattern = os.path.normpath(pattern).rstrip(os.path.sep)+os.path.sep+'*'+os.path.sep
         else:
@@ -281,11 +294,17 @@ def __init__(self, pattern):
 
     @normalized
     def match(self, path):
-        return self.regex.match(path+os.path.sep) is not None
+        matches = self.regex.match(path+os.path.sep) is not None
+        if matches:
+            self.match_count += 1
+        return matches
 
     def __repr__(self):
         return '%s(%s)' % (type(self), self.pattern)
 
+    def __str__(self):
+        return self.pattern_orig
+
 
 def timestamp(s):
     """Convert a --timestamp=s argument to a datetime object"""


Test patch: