diff --git a/src/borg/archive.py b/src/borg/archive.py index 0044bd53b..263536f20 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -31,7 +31,7 @@ from .helpers import format_time, format_timedelta, format_file_size, file_statu from .helpers import safe_encode, safe_decode, make_path_safe, remove_surrogates, swidth_slice from .helpers import decode_dict, StableDict from .helpers import int_to_bigint, bigint_to_int, bin_to_hex -from .helpers import ProgressIndicatorPercent, log_multi +from .helpers import ellipsis_truncate, ProgressIndicatorPercent, log_multi from .helpers import PathPrefixPattern, FnmatchPattern from .helpers import consume, chunkit from .helpers import CompressionDecider1, CompressionDecider2, CompressionSpec @@ -93,11 +93,7 @@ class Statistics: msg = '' space = columns - swidth(msg) if space >= 8: - if space < swidth('...') + swidth(path): - path = '%s...%s' % (swidth_slice(path, space // 2 - swidth('...')), - swidth_slice(path, -space // 2)) - space -= swidth(path) - msg += path + ' ' * space + msg += ellipsis_truncate(path, space) else: msg = ' ' * columns print(msg, file=stream or sys.stderr, end="\r", flush=True) @@ -448,7 +444,7 @@ Number of files: {0.stats.nfiles}'''.format( if 'chunks' in item: for _, data in self.pipeline.fetch_many([c.id for c in item.chunks], is_preloaded=True): if pi: - pi.show(increase=len(data)) + pi.show(increase=len(data), info=[remove_surrogates(item.path)]) if stdout: sys.stdout.buffer.write(data) if stdout: @@ -501,7 +497,7 @@ Number of files: {0.stats.nfiles}'''.format( ids = [c.id for c in item.chunks] for _, data in self.pipeline.fetch_many(ids, is_preloaded=True): if pi: - pi.show(increase=len(data)) + pi.show(increase=len(data), info=[remove_surrogates(item.path)]) with backup_io(): if sparse and self.zeros.startswith(data): # all-zero chunk: create a hole in a sparse file diff --git a/src/borg/archiver.py b/src/borg/archiver.py index f4d5bba39..6df3284c3 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -501,7 +501,7 @@ class Archiver: filter = self.build_filter(matcher, peek_and_store_hardlink_masters, strip_components) if progress: - pi = ProgressIndicatorPercent(msg='Extracting files %5.1f%%', step=0.1) + pi = ProgressIndicatorPercent(msg='%5.1f%% Extracting: %s', step=0.1) pi.output('Calculating size') extracted_size = sum(item.file_size(hardlink_masters) for item in archive.iter_items(filter)) pi.total = extracted_size @@ -546,6 +546,9 @@ class Archiver: for pattern in include_patterns: if pattern.match_count == 0: self.print_warning("Include pattern '%s' never matched.", pattern) + if pi: + # clear progress output + pi.finish() return self.exit_code @with_repository() diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 0b322685a..ab2e1271a 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -26,6 +26,7 @@ from functools import wraps, partial, lru_cache from itertools import islice from operator import attrgetter from string import Formatter +from shutil import get_terminal_size import msgpack import msgpack.fallback @@ -1191,6 +1192,23 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, env_var_override = None +def ellipsis_truncate(msg, space): + """ + shorten a long string by adding ellipsis between it and return it, example: + this_is_a_very_long_string -------> this_is..._string + """ + from .platform import swidth + ellipsis_width = swidth('...') + msg_width = swidth(msg) + if space < 8: + # if there is very little space, just show ... + return '...' + ' ' * (space - ellipsis_width) + if space < ellipsis_width + msg_width: + return '%s...%s' % (swidth_slice(msg, space // 2 - ellipsis_width), + swidth_slice(msg, -space // 2)) + return msg + ' ' * (space - msg_width) + + class ProgressIndicatorPercent: LOGGER = 'borg.output.progress' @@ -1208,7 +1226,6 @@ class ProgressIndicatorPercent: self.trigger_at = start # output next percentage value when reaching (at least) this self.step = step self.msg = msg - self.output_len = len(self.msg % 100.0) self.handler = None self.logger = logging.getLogger(self.LOGGER) @@ -1239,14 +1256,33 @@ class ProgressIndicatorPercent: self.trigger_at += self.step return pct - def show(self, current=None, increase=1): + def show(self, current=None, increase=1, info=None): + """ + Show and output the progress message + + :param current: set the current percentage [None] + :param increase: increase the current percentage [None] + :param info: array of strings to be formatted with msg [None] + """ pct = self.progress(current, increase) if pct is not None: + # truncate the last argument, if no space is available + if info is not None: + # no need to truncate if we're not outputing to a terminal + terminal_space = get_terminal_size(fallback=(-1, -1))[0] + if terminal_space != -1: + space = terminal_space - len(self.msg % tuple([pct] + info[:-1] + [''])) + info[-1] = ellipsis_truncate(info[-1], space) + return self.output(self.msg % tuple([pct] + info), justify=False) + return self.output(self.msg % pct) - def output(self, message): - self.output_len = max(len(message), self.output_len) - message = message.ljust(self.output_len) + def output(self, message, justify=True): + if justify: + terminal_space = get_terminal_size(fallback=(-1, -1))[0] + # no need to ljust if we're not outputing to a terminal + if terminal_space != -1: + message = message.ljust(terminal_space) self.logger.info(message) def finish(self): diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index d59d05f11..ffa7cccd6 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -774,7 +774,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with changedir('output'): output = self.cmd('extract', self.repository_location + '::test', '--progress') - assert 'Extracting files' in output + assert 'Extracting:' in output def _create_test_caches(self): self.cmd('init', self.repository_location) diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index bd4865f0d..f41cb4425 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -903,7 +903,10 @@ def test_yes_env_output(capfd, monkeypatch): assert 'yes' in err -def test_progress_percentage_sameline(capfd): +def test_progress_percentage_sameline(capfd, monkeypatch): + # run the test as if it was in a 4x1 terminal + monkeypatch.setenv('COLUMNS', '4') + monkeypatch.setenv('LINES', '1') pi = ProgressIndicatorPercent(1000, step=5, start=0, msg="%3.0f%%") pi.logger.setLevel('INFO') pi.show(0) @@ -921,7 +924,10 @@ def test_progress_percentage_sameline(capfd): assert err == ' ' * 4 + '\r' -def test_progress_percentage_step(capfd): +def test_progress_percentage_step(capfd, monkeypatch): + # run the test as if it was in a 4x1 terminal + monkeypatch.setenv('COLUMNS', '4') + monkeypatch.setenv('LINES', '1') pi = ProgressIndicatorPercent(100, step=2, start=0, msg="%3.0f%%") pi.logger.setLevel('INFO') pi.show()