Repository: borgbackup/borg

Issue:
Progress output generates extra lines with some filenames
I'm running `borg create --no-files-cache --progress --numeric-owner` in an interactive terminal, and with most filenames, the command shows only one line of progress with some counters and the current filename. However, with some filenames, it seems to go on to the next line instead of overwriting the current line. Basically, in the screenshot below, I expect to see only the last line, not all the previous lines.

![image](https://user-images.githubusercontent.com/15314936/30412513-063b9a3e-98e6-11e7-85ff-efd61a011259.png)

Discussion:

Patch:
diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py
index 93f93107..773632be 100644
--- a/src/borg/archiver/create_cmd.py
+++ b/src/borg/archiver/create_cmd.py
@@ -635,13 +635,13 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser):
         The ``-x`` or ``--one-file-system`` option excludes directories, that are mountpoints (and everything in them).
         It detects mountpoints by comparing the device number from the output of ``stat()`` of the directory and its
         parent directory. Specifically, it excludes directories for which ``stat()`` reports a device number different
-        from the device number of their parent.
-        In general: be aware that there are directories with device number different from their parent, which the kernel
-        does not consider a mountpoint and also the other way around.
-        Linux examples for this are bind mounts (possibly same device number, but always a mountpoint) and ALL
-        subvolumes of a btrfs (different device number from parent but not necessarily a mountpoint).
-        macOS examples are the apfs mounts of a typical macOS installation.
-        Therefore, when using ``--one-file-system``, you should double-check that the backup works as intended.
+        from the device number of their parent. Be aware that in Linux (and possibly elsewhere) there are directories
+        with device number different from their parent, which the kernel does not consider a mountpoint and also the
+        other way around. Examples are bind mounts (possibly same device number, but always a mountpoint) and ALL
+        subvolumes of a btrfs (different device number from parent but not necessarily a mountpoint). Therefore when
+        using ``--one-file-system``, one should make doubly sure that the backup works as intended especially when using
+        btrfs. This is even more important, if the btrfs layout was created by someone else, e.g. a distribution
+        installer.
 
 
         .. _list_item_flags:
@@ -819,8 +819,7 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser):
             "--one-file-system",
             dest="one_file_system",
             action="store_true",
-            help="stay in the same file system and do not store mount points of other file systems - "
-            "this might behave different from your expectations, see the description below.",
+            help="stay in the same file system and do not store mount points of other file systems.  This might behave different from your expectations, see the docs.",
         )
         fs_group.add_argument(
             "--numeric-ids",
diff --git a/src/borg/fuse.py b/src/borg/fuse.py
index a748aec1..4991d63d 100644
--- a/src/borg/fuse.py
+++ b/src/borg/fuse.py
@@ -562,14 +562,14 @@ def pop_option(options, key, present, not_present, wanted_type, int_base=0):
     @async_wrapper
     def statfs(self, ctx=None):
         stat_ = llfuse.StatvfsData()
-        stat_.f_bsize = 512  # Filesystem block size
-        stat_.f_frsize = 512  # Fragment size
-        stat_.f_blocks = 0  # Size of fs in f_frsize units
-        stat_.f_bfree = 0  # Number of free blocks
-        stat_.f_bavail = 0  # Number of free blocks for unprivileged users
-        stat_.f_files = 0  # Number of inodes
-        stat_.f_ffree = 0  # Number of free inodes
-        stat_.f_favail = 0  # Number of free inodes for unprivileged users
+        stat_.f_bsize = 512
+        stat_.f_frsize = 512
+        stat_.f_blocks = 0
+        stat_.f_bfree = 0
+        stat_.f_bavail = 0
+        stat_.f_files = 0
+        stat_.f_ffree = 0
+        stat_.f_favail = 0
         stat_.f_namemax = 255  # == NAME_MAX (depends on archive source OS / FS)
         return stat_
 
diff --git a/src/borg/helpers/progress.py b/src/borg/helpers/progress.py
index fb2e0fc5..782c7fb3 100644
--- a/src/borg/helpers/progress.py
+++ b/src/borg/helpers/progress.py
@@ -144,10 +144,12 @@ def show(self, current=None, increase=1, info=None):
             # truncate the last argument, if no space is available
             if info is not None:
                 if not self.json:
+                    from ..platform import swidth  # avoid circular import
+
                     # no need to truncate if we're not outputting 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] + [""]))
+                        space = terminal_space - swidth(self.msg % tuple([pct] + info[:-1] + [""]))
                         info[-1] = ellipsis_truncate(info[-1], space)
                 return self.output(self.msg % tuple([pct] + info), justify=False, info=info)
 


Test patch:
diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py
index 179e778b..df9b2ea2 100644
--- a/src/borg/testsuite/helpers.py
+++ b/src/borg/testsuite/helpers.py
@@ -45,7 +45,7 @@
 from ..helpers import safe_unlink
 from ..helpers import text_to_json, binary_to_json
 from ..helpers.passphrase import Passphrase, PasswordRetriesExceeded
-from ..platform import is_cygwin, is_win32, is_darwin
+from ..platform import is_cygwin, is_win32, is_darwin, swidth
 
 from . import BaseTestCase, FakeInputs, are_hardlinks_supported
 
@@ -1017,6 +1017,29 @@ def test_progress_percentage_sameline(capfd, monkeypatch):
     assert err == " " * 4 + "\r"
 
 
+@pytest.mark.skipif(is_win32, reason="no working swidth() implementation on this platform")
+def test_progress_percentage_widechars(capfd, monkeypatch):
+    st = "スター・トレック"  # "startrek" :-)
+    assert swidth(st) == 16
+    path = "/カーク船長です。"  # "Captain Kirk"
+    assert swidth(path) == 17
+    spaces = " " * 4  # to avoid usage of "..."
+    width = len("100%") + 1 + swidth(st) + 1 + swidth(path) + swidth(spaces)
+    monkeypatch.setenv("COLUMNS", str(width))
+    monkeypatch.setenv("LINES", "1")
+    pi = ProgressIndicatorPercent(100, step=5, start=0, msg=f"%3.0f%% {st} %s")
+    pi.logger.setLevel("INFO")
+    pi.show(0, info=[path])
+    out, err = capfd.readouterr()
+    assert err == f"  0% {st} {path}{spaces}\r"
+    pi.show(100, info=[path])
+    out, err = capfd.readouterr()
+    assert err == f"100% {st} {path}{spaces}\r"
+    pi.finish()
+    out, err = capfd.readouterr()
+    assert err == " " * width + "\r"
+
+
 def test_progress_percentage_step(capfd, monkeypatch):
     # run the test as if it was in a 4x1 terminal
     monkeypatch.setenv("COLUMNS", "4")