Browse Source

Print implied output without --info/-v

There are persistent questions why output from options like --list
and --stats doesn't show up.  Also, borg currently isn't able to
show *just* the output for a given option (--list, --stats,
--show-rc, --show-version, or --progress), without other INFO level
messages.

The solution is to use more granular loggers, so that messages
specific to a given option goes to a logger designated for that
option.  That option-specific logger can then be configured
separately from the regular loggers.

Those option-specific loggers can also be used as a hook in a
BORG_LOGGING_CONF config file to log the --list output to a separate
file, or send --stats output to a network socket where some daemon
could analyze it.

Steps:
- create an option-specific logger for each of the implied output options
- modify the messages specific to each option to go to the correct logger
- if an implied output option is passed, change the option-specific
  logger (only) to log at INFO level
- test that root logger messages don't come through option-specific loggers

They shouldn't, per https://docs.python.org/3/howto/logging.html#logging-flow
but test just the same.  Particularly test a message that can come from
remote repositories.

Fixes #526, #573, #665, #824
Ed Blackman 9 years ago
parent
commit
226e5519f3
8 changed files with 187 additions and 60 deletions
  1. 34 13
      borg/archiver.py
  2. 24 8
      borg/helpers.py
  3. 85 17
      borg/testsuite/archiver.py
  4. 22 4
      borg/testsuite/helpers.py
  5. 5 1
      docs/changes.rst
  6. 4 2
      docs/development.rst
  7. 6 5
      docs/quickstart.rst
  8. 7 10
      docs/usage.rst

+ 34 - 13
borg/archiver.py

@@ -8,6 +8,7 @@ import functools
 import hashlib
 import hashlib
 import inspect
 import inspect
 import io
 import io
+import logging
 import os
 import os
 import re
 import re
 import shlex
 import shlex
@@ -111,7 +112,7 @@ class Archiver:
 
 
     def print_file_status(self, status, path):
     def print_file_status(self, status, path):
         if self.output_list and (self.output_filter is None or status in self.output_filter):
         if self.output_list and (self.output_filter is None or status in self.output_filter):
-            logger.info("%1s %s", status, remove_surrogates(path))
+            logging.getLogger('borg.output.list').info("%1s %s", status, remove_surrogates(path))
 
 
     @staticmethod
     @staticmethod
     def compare_chunk_contents(chunks1, chunks2):
     def compare_chunk_contents(chunks1, chunks2):
@@ -277,7 +278,7 @@ class Archiver:
                               DASHES,
                               DASHES,
                               str(archive.stats),
                               str(archive.stats),
                               str(cache),
                               str(cache),
-                              DASHES)
+                              DASHES, logger=logging.getLogger('borg.output.stats'))
 
 
         self.output_filter = args.output_filter
         self.output_filter = args.output_filter
         self.output_list = args.output_list
         self.output_list = args.output_list
@@ -416,7 +417,7 @@ class Archiver:
                 while dirs and not item[b'path'].startswith(dirs[-1][b'path']):
                 while dirs and not item[b'path'].startswith(dirs[-1][b'path']):
                     archive.extract_item(dirs.pop(-1), stdout=stdout)
                     archive.extract_item(dirs.pop(-1), stdout=stdout)
             if output_list:
             if output_list:
-                logger.info(remove_surrogates(orig_path))
+                logging.getLogger('borg.output.list').info(remove_surrogates(orig_path))
             try:
             try:
                 if dry_run:
                 if dry_run:
                     archive.extract_item(item, dry_run=True)
                     archive.extract_item(item, dry_run=True)
@@ -673,7 +674,7 @@ class Archiver:
                     log_multi(DASHES,
                     log_multi(DASHES,
                               stats.summary.format(label='Deleted data:', stats=stats),
                               stats.summary.format(label='Deleted data:', stats=stats),
                               str(cache),
                               str(cache),
-                              DASHES)
+                              DASHES, logger=logging.getLogger('borg.output.stats'))
         else:
         else:
             if not args.cache_only:
             if not args.cache_only:
                 msg = []
                 msg = []
@@ -820,18 +821,19 @@ class Archiver:
         to_delete = (set(archives) | checkpoints) - (set(keep) | set(keep_checkpoints))
         to_delete = (set(archives) | checkpoints) - (set(keep) | set(keep_checkpoints))
         stats = Statistics()
         stats = Statistics()
         with Cache(repository, key, manifest, do_files=args.cache_files, lock_wait=self.lock_wait) as cache:
         with Cache(repository, key, manifest, do_files=args.cache_files, lock_wait=self.lock_wait) as cache:
+            list_logger = logging.getLogger('borg.output.list')
             for archive in archives_checkpoints:
             for archive in archives_checkpoints:
                 if archive in to_delete:
                 if archive in to_delete:
                     if args.dry_run:
                     if args.dry_run:
                         if args.output_list:
                         if args.output_list:
-                            logger.info('Would prune:     %s' % format_archive(archive))
+                            list_logger.info('Would prune:     %s' % format_archive(archive))
                     else:
                     else:
                         if args.output_list:
                         if args.output_list:
-                            logger.info('Pruning archive: %s' % format_archive(archive))
+                            list_logger.info('Pruning archive: %s' % format_archive(archive))
                         Archive(repository, key, manifest, archive.name, cache).delete(stats)
                         Archive(repository, key, manifest, archive.name, cache).delete(stats)
                 else:
                 else:
                     if args.output_list:
                     if args.output_list:
-                        logger.info('Keeping archive: %s' % format_archive(archive))
+                        list_logger.info('Keeping archive: %s' % format_archive(archive))
             if to_delete and not args.dry_run:
             if to_delete and not args.dry_run:
                 manifest.write()
                 manifest.write()
                 repository.commit(save_space=args.save_space)
                 repository.commit(save_space=args.save_space)
@@ -840,7 +842,7 @@ class Archiver:
                 log_multi(DASHES,
                 log_multi(DASHES,
                           stats.summary.format(label='Deleted data:', stats=stats),
                           stats.summary.format(label='Deleted data:', stats=stats),
                           str(cache),
                           str(cache),
-                          DASHES)
+                          DASHES, logger=logging.getLogger('borg.output.stats'))
         return self.exit_code
         return self.exit_code
 
 
     def do_upgrade(self, args):
     def do_upgrade(self, args):
@@ -1299,6 +1301,9 @@ class Archiver:
                                help='only check last N archives (Default: all)')
                                help='only check last N archives (Default: all)')
         subparser.add_argument('-P', '--prefix', dest='prefix', type=str,
         subparser.add_argument('-P', '--prefix', dest='prefix', type=str,
                                help='only consider archive names starting with this prefix')
                                help='only consider archive names starting with this prefix')
+        subparser.add_argument('-p', '--progress', dest='progress',
+                               action='store_true', default=False,
+                               help="""show progress display while checking""")
 
 
         change_passphrase_epilog = textwrap.dedent("""
         change_passphrase_epilog = textwrap.dedent("""
         The key files used for repository encryption are optionally passphrase
         The key files used for repository encryption are optionally passphrase
@@ -2065,12 +2070,27 @@ class Archiver:
         check_extension_modules()
         check_extension_modules()
         selftest(logger)
         selftest(logger)
 
 
+    def _setup_implied_logging(self, args):
+        """ turn on INFO level logging for args that imply that they will produce output """
+        # map of option name to name of logger for that option
+        option_logger = {
+                'output_list': 'borg.output.list',
+                'show_version': 'borg.output.show-version',
+                'show_rc': 'borg.output.show-rc',
+                'stats': 'borg.output.stats',
+                'progress': 'borg.output.progress',
+                }
+        for option, logger_name in option_logger.items():
+            if args.get(option, False):
+                logging.getLogger(logger_name).setLevel('INFO')
+
     def run(self, args):
     def run(self, args):
         os.umask(args.umask)  # early, before opening files
         os.umask(args.umask)  # early, before opening files
         self.lock_wait = args.lock_wait
         self.lock_wait = args.lock_wait
         setup_logging(level=args.log_level, is_serve=args.func == self.do_serve)  # do not use loggers before this!
         setup_logging(level=args.log_level, is_serve=args.func == self.do_serve)  # do not use loggers before this!
+        self._setup_implied_logging(vars(args))
         if args.show_version:
         if args.show_version:
-            logger.info('borgbackup version %s' % __version__)
+            logging.getLogger('borg.output.show-version').info('borgbackup version %s' % __version__)
         self.prerun_checks(logger)
         self.prerun_checks(logger)
         if is_slow_msgpack():
         if is_slow_msgpack():
             logger.warning("Using a pure-python msgpack! This will result in lower performance.")
             logger.warning("Using a pure-python msgpack! This will result in lower performance.")
@@ -2142,15 +2162,16 @@ def main():  # pragma: no cover
     if msg:
     if msg:
         logger.error(msg)
         logger.error(msg)
     if args.show_rc:
     if args.show_rc:
+        rc_logger = logging.getLogger('borg.output.show-rc')
         exit_msg = 'terminating with %s status, rc %d'
         exit_msg = 'terminating with %s status, rc %d'
         if exit_code == EXIT_SUCCESS:
         if exit_code == EXIT_SUCCESS:
-            logger.info(exit_msg % ('success', exit_code))
+            rc_logger.info(exit_msg % ('success', exit_code))
         elif exit_code == EXIT_WARNING:
         elif exit_code == EXIT_WARNING:
-            logger.warning(exit_msg % ('warning', exit_code))
+            rc_logger.warning(exit_msg % ('warning', exit_code))
         elif exit_code == EXIT_ERROR:
         elif exit_code == EXIT_ERROR:
-            logger.error(exit_msg % ('error', exit_code))
+            rc_logger.error(exit_msg % ('error', exit_code))
         else:
         else:
-            logger.error(exit_msg % ('abnormal', exit_code or 666))
+            rc_logger.error(exit_msg % ('abnormal', exit_code or 666))
     sys.exit(exit_code)
     sys.exit(exit_code)
 
 
 
 

+ 24 - 8
borg/helpers.py

@@ -1037,7 +1037,7 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None,
 
 
 
 
 class ProgressIndicatorPercent:
 class ProgressIndicatorPercent:
-    def __init__(self, total, step=5, start=0, same_line=False, msg="%3.0f%%", file=None):
+    def __init__(self, total, step=5, start=0, same_line=False, msg="%3.0f%%"):
         """
         """
         Percentage-based progress indicator
         Percentage-based progress indicator
 
 
@@ -1046,17 +1046,33 @@ class ProgressIndicatorPercent:
         :param start: at which percent value to start
         :param start: at which percent value to start
         :param same_line: if True, emit output always on same line
         :param same_line: if True, emit output always on same line
         :param msg: output message, must contain one %f placeholder for the percentage
         :param msg: output message, must contain one %f placeholder for the percentage
-        :param file: output file, default: sys.stderr
         """
         """
         self.counter = 0  # 0 .. (total-1)
         self.counter = 0  # 0 .. (total-1)
         self.total = total
         self.total = total
         self.trigger_at = start  # output next percentage value when reaching (at least) this
         self.trigger_at = start  # output next percentage value when reaching (at least) this
         self.step = step
         self.step = step
-        if file is None:
-            file = sys.stderr
-        self.file = file
         self.msg = msg
         self.msg = msg
         self.same_line = same_line
         self.same_line = same_line
+        self.handler = None
+        self.logger = logging.getLogger('borg.output.progress')
+
+        # If there are no handlers, set one up explicitly because the
+        # terminator and propagation needs to be set.  If there are,
+        # they must have been set up by BORG_LOGGING_CONF: skip setup.
+        if not self.logger.handlers:
+            self.handler = logging.StreamHandler(stream=sys.stderr)
+            self.handler.setLevel(logging.INFO)
+            self.handler.terminator = '\r' if self.same_line else '\n'
+
+            self.logger.addHandler(self.handler)
+            if self.logger.level == logging.NOTSET:
+                self.logger.setLevel(logging.WARN)
+            self.logger.propagate = False
+
+    def __del__(self):
+        if self.handler is not None:
+            self.logger.removeHandler(self.handler)
+            self.handler.close()
 
 
     def progress(self, current=None):
     def progress(self, current=None):
         if current is not None:
         if current is not None:
@@ -1073,11 +1089,11 @@ class ProgressIndicatorPercent:
             return self.output(pct)
             return self.output(pct)
 
 
     def output(self, percent):
     def output(self, percent):
-        print(self.msg % percent, file=self.file, end='\r' if self.same_line else '\n', flush=True)
+        self.logger.info(self.msg % percent)
 
 
     def finish(self):
     def finish(self):
         if self.same_line:
         if self.same_line:
-            print(" " * len(self.msg % 100.0), file=self.file, end='\r')
+            self.logger.info(" " * len(self.msg % 100.0))
 
 
 
 
 class ProgressIndicatorEndless:
 class ProgressIndicatorEndless:
@@ -1127,7 +1143,7 @@ def sysinfo():
     return '\n'.join(info)
     return '\n'.join(info)
 
 
 
 
-def log_multi(*msgs, level=logging.INFO):
+def log_multi(*msgs, level=logging.INFO, logger=logger):
     """
     """
     log multiple lines of text, each line by a separate logging call for cosmetic reasons
     log multiple lines of text, each line by a separate logging call for cosmetic reasons
 
 

+ 85 - 17
borg/testsuite/archiver.py

@@ -3,6 +3,7 @@ import errno
 import os
 import os
 import inspect
 import inspect
 from io import StringIO
 from io import StringIO
+import logging
 import random
 import random
 import stat
 import stat
 import subprocess
 import subprocess
@@ -297,9 +298,14 @@ class ArchiverTestCaseBase(BaseTestCase):
 class ArchiverTestCase(ArchiverTestCaseBase):
 class ArchiverTestCase(ArchiverTestCaseBase):
     def test_basic_functionality(self):
     def test_basic_functionality(self):
         have_root = self.create_test_files()
         have_root = self.create_test_files()
-        self.cmd('init', self.repository_location)
+        # fork required to test show-rc output
+        output = self.cmd('init', '--show-version', '--show-rc', self.repository_location, fork=True)
+        self.assert_in('borgbackup version', output)
+        self.assert_in('terminating with success status, rc 0', output)
         self.cmd('create', self.repository_location + '::test', 'input')
         self.cmd('create', self.repository_location + '::test', 'input')
-        self.cmd('create', '--stats', self.repository_location + '::test.2', 'input')
+        output = self.cmd('create', '--stats', self.repository_location + '::test.2', 'input')
+        self.assert_in('Archive name: test.2', output)
+        self.assert_in('This archive: ', output)
         with changedir('output'):
         with changedir('output'):
             self.cmd('extract', self.repository_location + '::test')
             self.cmd('extract', self.repository_location + '::test')
         list_output = self.cmd('list', '--short', self.repository_location)
         list_output = self.cmd('list', '--short', self.repository_location)
@@ -645,6 +651,31 @@ class ArchiverTestCase(ArchiverTestCaseBase):
             self.cmd("extract", self.repository_location + "::test", "fm:input/file1", "fm:*file33*", "input/file2")
             self.cmd("extract", self.repository_location + "::test", "fm:input/file1", "fm:*file33*", "input/file2")
         self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file333"])
         self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file333"])
 
 
+    def test_extract_list_output(self):
+        self.cmd('init', self.repository_location)
+        self.create_regular_file('file', size=1024 * 80)
+
+        self.cmd('create', self.repository_location + '::test', 'input')
+
+        with changedir('output'):
+            output = self.cmd('extract', self.repository_location + '::test')
+        self.assert_not_in("input/file", output)
+        shutil.rmtree('output/input')
+
+        with changedir('output'):
+            output = self.cmd('extract', '--info', self.repository_location + '::test')
+        self.assert_not_in("input/file", output)
+        shutil.rmtree('output/input')
+
+        with changedir('output'):
+            output = self.cmd('extract', '--list', self.repository_location + '::test')
+        self.assert_in("input/file", output)
+        shutil.rmtree('output/input')
+
+        with changedir('output'):
+            output = self.cmd('extract', '--list', '--info', self.repository_location + '::test')
+        self.assert_in("input/file", output)
+
     def _create_test_caches(self):
     def _create_test_caches(self):
         self.cmd('init', self.repository_location)
         self.cmd('init', self.repository_location)
         self.create_regular_file('file1', size=1024 * 80)
         self.create_regular_file('file1', size=1024 * 80)
@@ -857,7 +888,8 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
         self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
         self.cmd('delete', self.repository_location + '::test')
         self.cmd('delete', self.repository_location + '::test')
         self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
         self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
-        self.cmd('delete', '--stats', self.repository_location + '::test.2')
+        output = self.cmd('delete', '--stats', self.repository_location + '::test.2')
+        self.assert_in('Deleted data:', output)
         # Make sure all data except the manifest has been deleted
         # Make sure all data except the manifest has been deleted
         with Repository(self.repository_path) as repository:
         with Repository(self.repository_path) as repository:
             self.assert_equal(len(repository), 1)
             self.assert_equal(len(repository), 1)
@@ -880,12 +912,16 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd('init', self.repository_location)
         self.cmd('init', self.repository_location)
         self.create_src_archive('test')
         self.create_src_archive('test')
         self.cmd('extract', '--dry-run', self.repository_location + '::test')
         self.cmd('extract', '--dry-run', self.repository_location + '::test')
-        self.cmd('check', self.repository_location)
+        output = self.cmd('check', '--show-version', self.repository_location)
+        self.assert_in('borgbackup version', output)  # implied output even without --info given
+        self.assert_not_in('Starting repository check', output)  # --info not given for root logger
+
         name = sorted(os.listdir(os.path.join(self.tmpdir, 'repository', 'data', '0')), reverse=True)[0]
         name = sorted(os.listdir(os.path.join(self.tmpdir, 'repository', 'data', '0')), reverse=True)[0]
         with open(os.path.join(self.tmpdir, 'repository', 'data', '0', name), 'r+b') as fd:
         with open(os.path.join(self.tmpdir, 'repository', 'data', '0', name), 'r+b') as fd:
             fd.seek(100)
             fd.seek(100)
             fd.write(b'XXXX')
             fd.write(b'XXXX')
-        self.cmd('check', self.repository_location, exit_code=1)
+        output = self.cmd('check', '--info', self.repository_location, exit_code=1)
+        self.assert_in('Starting repository check', output)  # --info given for root logger
 
 
     # we currently need to be able to create a lock directory inside the repo:
     # we currently need to be able to create a lock directory inside the repo:
     @pytest.mark.xfail(reason="we need to be able to create the lock directory inside the repo")
     @pytest.mark.xfail(reason="we need to be able to create the lock directory inside the repo")
@@ -933,11 +969,11 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         os.utime('input/file1', (now - 5, now - 5))  # 5 seconds ago
         os.utime('input/file1', (now - 5, now - 5))  # 5 seconds ago
         self.create_regular_file('file2', size=1024 * 80)
         self.create_regular_file('file2', size=1024 * 80)
         self.cmd('init', self.repository_location)
         self.cmd('init', self.repository_location)
-        output = self.cmd('create', '-v', '--list', self.repository_location + '::test', 'input')
+        output = self.cmd('create', '--list', self.repository_location + '::test', 'input')
         self.assert_in("A input/file1", output)
         self.assert_in("A input/file1", output)
         self.assert_in("A input/file2", output)
         self.assert_in("A input/file2", output)
         # should find first file as unmodified
         # should find first file as unmodified
-        output = self.cmd('create', '-v', '--list', self.repository_location + '::test1', 'input')
+        output = self.cmd('create', '--list', self.repository_location + '::test1', 'input')
         self.assert_in("U input/file1", output)
         self.assert_in("U input/file1", output)
         # this is expected, although surprising, for why, see:
         # this is expected, although surprising, for why, see:
         # https://borgbackup.readthedocs.org/en/latest/faq.html#i-am-seeing-a-added-status-for-a-unchanged-file
         # https://borgbackup.readthedocs.org/en/latest/faq.html#i-am-seeing-a-added-status-for-a-unchanged-file
@@ -951,11 +987,11 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         os.utime('input/file1', (now - 5, now - 5))  # 5 seconds ago
         os.utime('input/file1', (now - 5, now - 5))  # 5 seconds ago
         self.create_regular_file('file2', size=1024 * 80)
         self.create_regular_file('file2', size=1024 * 80)
         self.cmd('init', self.repository_location)
         self.cmd('init', self.repository_location)
-        output = self.cmd('create', '-v', '--list', self.repository_location + '::test', 'input')
+        output = self.cmd('create', '--list', self.repository_location + '::test', 'input')
         self.assert_in("A input/file1", output)
         self.assert_in("A input/file1", output)
         self.assert_in("A input/file2", output)
         self.assert_in("A input/file2", output)
         # should find second file as excluded
         # should find second file as excluded
-        output = self.cmd('create', '-v', '--list', self.repository_location + '::test1', 'input', '--exclude', '*/file2')
+        output = self.cmd('create', '--list', self.repository_location + '::test1', 'input', '--exclude', '*/file2')
         self.assert_in("U input/file1", output)
         self.assert_in("U input/file1", output)
         self.assert_in("x input/file2", output)
         self.assert_in("x input/file2", output)
 
 
@@ -972,15 +1008,15 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         output = self.cmd('create', self.repository_location + '::test0', 'input')
         output = self.cmd('create', self.repository_location + '::test0', 'input')
         self.assert_not_in('file1', output)
         self.assert_not_in('file1', output)
         # should list the file as unchanged
         # should list the file as unchanged
-        output = self.cmd('create', '-v', '--list', '--filter=U', self.repository_location + '::test1', 'input')
+        output = self.cmd('create', '--list', '--filter=U', self.repository_location + '::test1', 'input')
         self.assert_in('file1', output)
         self.assert_in('file1', output)
         # should *not* list the file as changed
         # should *not* list the file as changed
-        output = self.cmd('create', '-v', '--filter=AM', self.repository_location + '::test2', 'input')
+        output = self.cmd('create', '--list', '--filter=AM', self.repository_location + '::test2', 'input')
         self.assert_not_in('file1', output)
         self.assert_not_in('file1', output)
         # change the file
         # change the file
         self.create_regular_file('file1', size=1024 * 100)
         self.create_regular_file('file1', size=1024 * 100)
         # should list the file as changed
         # should list the file as changed
-        output = self.cmd('create', '-v', '--list', '--filter=AM', self.repository_location + '::test3', 'input')
+        output = self.cmd('create', '--list', '--filter=AM', self.repository_location + '::test3', 'input')
         self.assert_in('file1', output)
         self.assert_in('file1', output)
 
 
     # def test_cmdline_compatibility(self):
     # def test_cmdline_compatibility(self):
@@ -998,7 +1034,8 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd('create', self.repository_location + '::test3.checkpoint', src_dir)
         self.cmd('create', self.repository_location + '::test3.checkpoint', src_dir)
         self.cmd('create', self.repository_location + '::test3.checkpoint.1', src_dir)
         self.cmd('create', self.repository_location + '::test3.checkpoint.1', src_dir)
         self.cmd('create', self.repository_location + '::test4.checkpoint', src_dir)
         self.cmd('create', self.repository_location + '::test4.checkpoint', src_dir)
-        output = self.cmd('prune', '-v', '--list', '--dry-run', self.repository_location, '--keep-daily=2')
+        output = self.cmd('prune', '--list', '--dry-run', self.repository_location, '--keep-daily=2')
+        self.assert_in('Keeping archive: test2', output)
         self.assert_in('Would prune:     test1', output)
         self.assert_in('Would prune:     test1', output)
         # must keep the latest non-checkpoint archive:
         # must keep the latest non-checkpoint archive:
         self.assert_in('Keeping archive: test2', output)
         self.assert_in('Keeping archive: test2', output)
@@ -1032,9 +1069,10 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd('init', self.repository_location)
         self.cmd('init', self.repository_location)
         self.cmd('create', self.repository_location + '::test1', src_dir)
         self.cmd('create', self.repository_location + '::test1', src_dir)
         self.cmd('create', self.repository_location + '::test2', src_dir)
         self.cmd('create', self.repository_location + '::test2', src_dir)
-        output = self.cmd('prune', '-v', '--list', '--dry-run', self.repository_location, '--keep-daily=2')
+        output = self.cmd('prune', '--list', '--stats', '--dry-run', self.repository_location, '--keep-daily=2')
         self.assert_in('Keeping archive: test2', output)
         self.assert_in('Keeping archive: test2', output)
         self.assert_in('Would prune:     test1', output)
         self.assert_in('Would prune:     test1', output)
+        self.assert_in('Deleted data:', output)
         output = self.cmd('list', self.repository_location)
         output = self.cmd('list', self.repository_location)
         self.assert_in('test1', output)
         self.assert_in('test1', output)
         self.assert_in('test2', output)
         self.assert_in('test2', output)
@@ -1049,7 +1087,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd('create', self.repository_location + '::foo-2015-08-12-20:00', src_dir)
         self.cmd('create', self.repository_location + '::foo-2015-08-12-20:00', src_dir)
         self.cmd('create', self.repository_location + '::bar-2015-08-12-10:00', src_dir)
         self.cmd('create', self.repository_location + '::bar-2015-08-12-10:00', src_dir)
         self.cmd('create', self.repository_location + '::bar-2015-08-12-20:00', src_dir)
         self.cmd('create', self.repository_location + '::bar-2015-08-12-20:00', src_dir)
-        output = self.cmd('prune', '-v', '--list', '--dry-run', self.repository_location, '--keep-daily=2', '--prefix=foo-')
+        output = self.cmd('prune', '--list', '--dry-run', self.repository_location, '--keep-daily=2', '--prefix=foo-')
         self.assert_in('Keeping archive: foo-2015-08-12-20:00', output)
         self.assert_in('Keeping archive: foo-2015-08-12-20:00', output)
         self.assert_in('Would prune:     foo-2015-08-12-10:00', output)
         self.assert_in('Would prune:     foo-2015-08-12-10:00', output)
         output = self.cmd('list', self.repository_location)
         output = self.cmd('list', self.repository_location)
@@ -1401,7 +1439,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         if interrupt_early:
         if interrupt_early:
             process_files = 0
             process_files = 0
         with patch.object(ArchiveRecreater, 'process_item', self._recreate_interrupt_patch(process_files)):
         with patch.object(ArchiveRecreater, 'process_item', self._recreate_interrupt_patch(process_files)):
-            self.cmd('recreate', '-sv', '--list', self.repository_location, 'input/dir2')
+            self.cmd('recreate', self.repository_location, 'input/dir2')
         assert 'test.recreate' in self.cmd('list', self.repository_location)
         assert 'test.recreate' in self.cmd('list', self.repository_location)
         if change_args:
         if change_args:
             with patch.object(sys, 'argv', sys.argv + ['non-forking tests don\'t use sys.argv']):
             with patch.object(sys, 'argv', sys.argv + ['non-forking tests don\'t use sys.argv']):
@@ -1492,6 +1530,32 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         cmd = 'python3', '-c', 'import os, sys; sys.exit(42 if os.path.exists("%s") else 23)' % lock_path
         cmd = 'python3', '-c', 'import os, sys; sys.exit(42 if os.path.exists("%s") else 23)' % lock_path
         self.cmd('with-lock', self.repository_location, *cmd, fork=True, exit_code=42)
         self.cmd('with-lock', self.repository_location, *cmd, fork=True, exit_code=42)
 
 
+    def test_recreate_list_output(self):
+        self.cmd('init', self.repository_location)
+        self.create_regular_file('file1', size=0)
+        self.create_regular_file('file2', size=0)
+        self.create_regular_file('file3', size=0)
+        self.create_regular_file('file4', size=0)
+        self.create_regular_file('file5', size=0)
+
+        self.cmd('create', self.repository_location + '::test', 'input')
+
+        output = self.cmd('recreate', '--list', '--info', self.repository_location + '::test', '-e', 'input/file2')
+        self.assert_in("input/file1", output)
+        self.assert_in("x input/file2", output)
+
+        output = self.cmd('recreate', '--list', self.repository_location + '::test', '-e', 'input/file3')
+        self.assert_in("input/file1", output)
+        self.assert_in("x input/file3", output)
+
+        output = self.cmd('recreate', self.repository_location + '::test', '-e', 'input/file4')
+        self.assert_not_in("input/file1", output)
+        self.assert_not_in("x input/file4", output)
+
+        output = self.cmd('recreate', '--info', self.repository_location + '::test', '-e', 'input/file5')
+        self.assert_not_in("input/file1", output)
+        self.assert_not_in("x input/file5", output)
+
 
 
 @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
 @unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
 class ArchiverTestCaseBinary(ArchiverTestCase):
 class ArchiverTestCaseBinary(ArchiverTestCase):
@@ -1532,12 +1596,16 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase):
         return archive, repository
         return archive, repository
 
 
     def test_check_usage(self):
     def test_check_usage(self):
-        output = self.cmd('check', '-v', self.repository_location, exit_code=0)
+        output = self.cmd('check', '-v', '--progress', self.repository_location, exit_code=0)
         self.assert_in('Starting repository check', output)
         self.assert_in('Starting repository check', output)
         self.assert_in('Starting archive consistency check', output)
         self.assert_in('Starting archive consistency check', output)
+        self.assert_in('Checking segments', output)
+        # reset logging to new process default to avoid need for fork=True on next check
+        logging.getLogger('borg.output.progress').setLevel(logging.NOTSET)
         output = self.cmd('check', '-v', '--repository-only', self.repository_location, exit_code=0)
         output = self.cmd('check', '-v', '--repository-only', self.repository_location, exit_code=0)
         self.assert_in('Starting repository check', output)
         self.assert_in('Starting repository check', output)
         self.assert_not_in('Starting archive consistency check', output)
         self.assert_not_in('Starting archive consistency check', output)
+        self.assert_not_in('Checking segments', output)
         output = self.cmd('check', '-v', '--archives-only', self.repository_location, exit_code=0)
         output = self.cmd('check', '-v', '--archives-only', self.repository_location, exit_code=0)
         self.assert_not_in('Starting repository check', output)
         self.assert_not_in('Starting repository check', output)
         self.assert_in('Starting archive consistency check', output)
         self.assert_in('Starting archive consistency check', output)

+ 22 - 4
borg/testsuite/helpers.py

@@ -1,4 +1,6 @@
 import hashlib
 import hashlib
+import io
+import logging
 from time import mktime, strptime
 from time import mktime, strptime
 from datetime import datetime, timezone, timedelta
 from datetime import datetime, timezone, timedelta
 from io import StringIO
 from io import StringIO
@@ -826,7 +828,7 @@ def test_yes_output(capfd):
 
 
 
 
 def test_progress_percentage_multiline(capfd):
 def test_progress_percentage_multiline(capfd):
-    pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=False, msg="%3.0f%%", file=sys.stderr)
+    pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=False, msg="%3.0f%%")
     pi.show(0)
     pi.show(0)
     out, err = capfd.readouterr()
     out, err = capfd.readouterr()
     assert err == '  0%\n'
     assert err == '  0%\n'
@@ -842,13 +844,14 @@ def test_progress_percentage_multiline(capfd):
 
 
 
 
 def test_progress_percentage_sameline(capfd):
 def test_progress_percentage_sameline(capfd):
-    pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=True, msg="%3.0f%%", file=sys.stderr)
+    pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=True, msg="%3.0f%%")
     pi.show(0)
     pi.show(0)
     out, err = capfd.readouterr()
     out, err = capfd.readouterr()
     assert err == '  0%\r'
     assert err == '  0%\r'
     pi.show(420)
     pi.show(420)
+    pi.show(680)
     out, err = capfd.readouterr()
     out, err = capfd.readouterr()
-    assert err == ' 42%\r'
+    assert err == ' 42%\r 68%\r'
     pi.show(1000)
     pi.show(1000)
     out, err = capfd.readouterr()
     out, err = capfd.readouterr()
     assert err == '100%\r'
     assert err == '100%\r'
@@ -858,7 +861,7 @@ def test_progress_percentage_sameline(capfd):
 
 
 
 
 def test_progress_percentage_step(capfd):
 def test_progress_percentage_step(capfd):
-    pi = ProgressIndicatorPercent(100, step=2, start=0, same_line=False, msg="%3.0f%%", file=sys.stderr)
+    pi = ProgressIndicatorPercent(100, step=2, start=0, same_line=False, msg="%3.0f%%")
     pi.show()
     pi.show()
     out, err = capfd.readouterr()
     out, err = capfd.readouterr()
     assert err == '  0%\n'
     assert err == '  0%\n'
@@ -870,6 +873,21 @@ def test_progress_percentage_step(capfd):
     assert err == '  2%\n'
     assert err == '  2%\n'
 
 
 
 
+def test_progress_percentage_quiet(capfd):
+    logging.getLogger('borg.output.progress').setLevel(logging.WARN)
+
+    pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=False, msg="%3.0f%%")
+    pi.show(0)
+    out, err = capfd.readouterr()
+    assert err == ''
+    pi.show(1000)
+    out, err = capfd.readouterr()
+    assert err == ''
+    pi.finish()
+    out, err = capfd.readouterr()
+    assert err == ''
+
+
 def test_progress_endless(capfd):
 def test_progress_endless(capfd):
     pi = ProgressIndicatorEndless(step=1, file=sys.stderr)
     pi = ProgressIndicatorEndless(step=1, file=sys.stderr)
     pi.show()
     pi.show()

+ 5 - 1
docs/changes.rst

@@ -6,6 +6,10 @@ Version 1.1.0 (not released yet)
 
 
 New features:
 New features:
 
 
+- borg check: will not produce the "Checking segments" output unless
+  new --progress option is passed, #824.
+- options that imply output (--show-rc, --show-version, --list, --stats, 
+  --progress) don't need -v/--info to have that output displayed, #865
 - borg recreate: re-create existing archives, #787 #686 #630 #70, also see
 - borg recreate: re-create existing archives, #787 #686 #630 #70, also see
   #757, #770.
   #757, #770.
 
 
@@ -34,7 +38,7 @@ New features:
 - borg comment: add archive comments, #842
 - borg comment: add archive comments, #842
 - provide "borgfs" wrapper for borg mount, enables usage via fstab, #743
 - provide "borgfs" wrapper for borg mount, enables usage via fstab, #743
 - create: add 'x' status for excluded paths, #814
 - create: add 'x' status for excluded paths, #814
-- --show-version: shows/logs the borg version (use -v), #725
+- --show-version: shows/logs the borg version, #725
 - borg list/prune/delete: also output archive id, #731
 - borg list/prune/delete: also output archive id, #731
 
 
 Bug fixes:
 Bug fixes:

+ 4 - 2
docs/development.rst

@@ -27,8 +27,10 @@ errors, critical for critical errors/states).
 When directly talking to the user (e.g. Y/N questions), do not use logging,
 When directly talking to the user (e.g. Y/N questions), do not use logging,
 but directly output to stderr (not: stdout, it could be connected to a pipe).
 but directly output to stderr (not: stdout, it could be connected to a pipe).
 
 
-To control the amount and kinds of messages output to stderr or emitted at
-info level, use flags like ``--stats`` or ``--list``.
+To control the amount and kinds of messages output emitted at info level, use
+flags like ``--stats`` or ``--list``, then create a topic logger for messages
+controlled by that flag.  See ``_setup_implied_logging()`` in
+``borg/archiver.py`` for the entry point to topic logging.
 
 
 Building a development environment
 Building a development environment
 ----------------------------------
 ----------------------------------

+ 6 - 5
docs/quickstart.rst

@@ -46,7 +46,7 @@ A step by step example
 
 
 3. The next day create a new archive called *Tuesday*::
 3. The next day create a new archive called *Tuesday*::
 
 
-    $ borg create -v --stats /path/to/repo::Tuesday ~/src ~/Documents
+    $ borg create --stats /path/to/repo::Tuesday ~/src ~/Documents
 
 
    This backup will be a lot quicker and a lot smaller since only new never
    This backup will be a lot quicker and a lot smaller since only new never
    before seen data is stored. The ``--stats`` option causes |project_name| to
    before seen data is stored. The ``--stats`` option causes |project_name| to
@@ -93,9 +93,10 @@ A step by step example
 
 
 .. Note::
 .. Note::
     Borg is quiet by default (it works on WARNING log level).
     Borg is quiet by default (it works on WARNING log level).
-    Add the ``-v`` (or ``--verbose`` or ``--info``) option to adjust the log
-    level to INFO and also use options like ``--progress`` or ``--list`` to
-    get progress reporting during command execution.
+    You can use options like ``--progress`` or ``--list`` to get specific
+    reports during command execution.  You can also add the ``-v`` (or
+    ``--verbose`` or ``--info``) option to adjust the log level to INFO to
+    get other informational messages.
 
 
 Automating backups
 Automating backups
 ------------------
 ------------------
@@ -114,7 +115,7 @@ certain number of old archives::
     export BORG_PASSPHRASE=mysecret
     export BORG_PASSPHRASE=mysecret
 
 
     # Backup most important stuff:
     # Backup most important stuff:
-    borg create -v --stats -C lz4 ::`hostname`-`date +%Y-%m-%d` \
+    borg create --stats -C lz4 ::`hostname`-`date +%Y-%m-%d` \
         /etc                                                    \
         /etc                                                    \
         /home                                                   \
         /home                                                   \
         /var                                                    \
         /var                                                    \

+ 7 - 10
docs/usage.rst

@@ -16,7 +16,8 @@ Type of log output
 
 
 The log level of the builtin logging configuration defaults to WARNING.
 The log level of the builtin logging configuration defaults to WARNING.
 This is because we want |project_name| to be mostly silent and only output
 This is because we want |project_name| to be mostly silent and only output
-warnings, errors and critical messages.
+warnings, errors and critical messages, unless output has been requested
+by supplying an option that implies output (eg, --list or --progress).
 
 
 Log levels: DEBUG < INFO < WARNING < ERROR < CRITICAL
 Log levels: DEBUG < INFO < WARNING < ERROR < CRITICAL
 
 
@@ -41,10 +42,6 @@ give different output on different log levels - it's just a possibility.
 .. warning:: Options --critical and --error are provided for completeness,
 .. warning:: Options --critical and --error are provided for completeness,
              their usage is not recommended as you might miss important information.
              their usage is not recommended as you might miss important information.
 
 
-.. warning:: While some options (like ``--stats`` or ``--list``) will emit more
-             informational messages, you have to use INFO (or lower) log level to make
-             them show up in log output. Use ``-v`` or a logging configuration.
-
 Return codes
 Return codes
 ~~~~~~~~~~~~
 ~~~~~~~~~~~~
 
 
@@ -245,8 +242,8 @@ Examples
     # Backup ~/Documents into an archive named "my-documents"
     # Backup ~/Documents into an archive named "my-documents"
     $ borg create /path/to/repo::my-documents ~/Documents
     $ borg create /path/to/repo::my-documents ~/Documents
 
 
-    # same, but verbosely list all files as we process them
-    $ borg create -v --list /path/to/repo::my-documents ~/Documents
+    # same, but list all files as we process them
+    $ borg create --list /path/to/repo::my-documents ~/Documents
 
 
     # Backup ~/Documents and ~/src but exclude pyc files
     # Backup ~/Documents and ~/src but exclude pyc files
     $ borg create /path/to/repo::my-files \
     $ borg create /path/to/repo::my-files \
@@ -301,7 +298,7 @@ Examples
     $ borg extract /path/to/repo::my-files
     $ borg extract /path/to/repo::my-files
 
 
     # Extract entire archive and list files while processing
     # Extract entire archive and list files while processing
-    $ borg extract -v --list /path/to/repo::my-files
+    $ borg extract --list /path/to/repo::my-files
 
 
     # Verify whether an archive could be successfully extracted, but do not write files to disk
     # Verify whether an archive could be successfully extracted, but do not write files to disk
     $ borg extract --dry-run /path/to/repo::my-files
     $ borg extract --dry-run /path/to/repo::my-files
@@ -480,7 +477,7 @@ Examples
     Username: root
     Username: root
     Time (start): Mon, 2016-02-15 19:36:29
     Time (start): Mon, 2016-02-15 19:36:29
     Time (end):   Mon, 2016-02-15 19:39:26
     Time (end):   Mon, 2016-02-15 19:39:26
-    Command line: /usr/local/bin/borg create -v --list -C zlib,6 /path/to/repo::root-2016-02-15 / --one-file-system
+    Command line: /usr/local/bin/borg create --list -C zlib,6 /path/to/repo::root-2016-02-15 / --one-file-system
     Number of files: 38100
     Number of files: 38100
 
 
                            Original size      Compressed size    Deduplicated size
                            Original size      Compressed size    Deduplicated size
@@ -655,7 +652,7 @@ Here are misc. notes about topics that are maybe not covered in enough detail in
 Item flags
 Item flags
 ~~~~~~~~~~
 ~~~~~~~~~~
 
 
-``borg create -v --list`` outputs a verbose list of all files, directories and other
+``borg create --list`` outputs a list of all files, directories and other
 file system items it considered (no matter whether they had content changes
 file system items it considered (no matter whether they had content changes
 or not). For each item, it prefixes a single-letter flag that indicates type
 or not). For each item, it prefixes a single-letter flag that indicates type
 and/or status of the item.
 and/or status of the item.