Repository: borgbackup/borg

Issue:
borgfs still shows 'mount' in options
When running `borgfs --help`, `borgfs mount`, `borgfs`, etc. the short usage still shows that you should put `mount` after borgfs:

    usage: borgfs mount [-h] [--critical] [--error] [--warning] [--info] [--debug]
                        [--debug-topic TOPIC] [-p] [--log-json]
                        [--lock-wait SECONDS] [--show-version] [--show-rc]
                        [--umask M] [--remote-path PATH] [--remote-ratelimit RATE]
                        [--consider-part-files] [--debug-profile FILE] [-f]
                        [-o OPTIONS] [-P PREFIX | -a GLOB] [--sort-by KEYS]
                        [--first N | --last N]
                        REPOSITORY_OR_ARCHIVE MOUNTPOINT

Whereas the first line should just be:

    usage: borgfs [-h] [--critical] [--error] [--warning] [--info] [--debug]

borgfs is missing a man page
and therefore, it is listed there:

https://qa.debian.org/man-pages.html


Discussion:

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -242,10 +242,11 @@ def run(self):
         # allows us to build docs without the C modules fully loaded during help generation
         from borg.archiver import Archiver
         parser = Archiver(prog='borg').build_parser()
+        borgfs_parser = Archiver(prog='borgfs').build_parser()
 
-        self.generate_level("", parser, Archiver)
+        self.generate_level("", parser, Archiver, {'borgfs': borgfs_parser})
 
-    def generate_level(self, prefix, parser, Archiver):
+    def generate_level(self, prefix, parser, Archiver, extra_choices=None):
         is_subcommand = False
         choices = {}
         for action in parser._actions:
@@ -253,6 +254,8 @@ def generate_level(self, prefix, parser, Archiver):
                 is_subcommand = True
                 for cmd, parser in action.choices.items():
                     choices[prefix + cmd] = parser
+        if extra_choices is not None:
+            choices.update(extra_choices)
         if prefix and not choices:
             return
         print('found commands: %s' % list(choices.keys()))
@@ -505,12 +508,13 @@ def run(self):
         # allows us to build docs without the C modules fully loaded during help generation
         from borg.archiver import Archiver
         parser = Archiver(prog='borg').build_parser()
+        borgfs_parser = Archiver(prog='borgfs').build_parser()
 
-        self.generate_level('', parser, Archiver)
+        self.generate_level('', parser, Archiver, {'borgfs': borgfs_parser})
         self.build_topic_pages(Archiver)
         self.build_intro_page()
 
-    def generate_level(self, prefix, parser, Archiver):
+    def generate_level(self, prefix, parser, Archiver, extra_choices=None):
         is_subcommand = False
         choices = {}
         for action in parser._actions:
@@ -518,6 +522,8 @@ def generate_level(self, prefix, parser, Archiver):
                 is_subcommand = True
                 for cmd, parser in action.choices.items():
                     choices[prefix + cmd] = parser
+        if extra_choices is not None:
+            choices.update(extra_choices)
         if prefix and not choices:
             return
 
@@ -525,7 +531,10 @@ def generate_level(self, prefix, parser, Archiver):
             if command.startswith('debug') or command == 'help':
                 continue
 
-            man_title = 'borg-' + command.replace(' ', '-')
+            if command == "borgfs":
+                man_title = command
+            else:
+                man_title = 'borg-' + command.replace(' ', '-')
             print('building man page', man_title + '(1)', file=sys.stderr)
 
             is_intermediary = self.generate_level(command + ' ', parser, Archiver)
@@ -540,7 +549,10 @@ def generate_level(self, prefix, parser, Archiver):
                     write('| borg', '[common options]', command, subcommand, '...')
                     self.see_also.setdefault(command, []).append('%s-%s' % (command, subcommand))
             else:
-                write('borg', '[common options]', command, end='')
+                if command == "borgfs":
+                    write(command, end='')
+                else:
+                    write('borg', '[common options]', command, end='')
                 self.write_usage(write, parser)
             write('\n')
 
diff --git a/src/borg/archiver.py b/src/borg/archiver.py
--- a/src/borg/archiver.py
+++ b/src/borg/archiver.py
@@ -2266,7 +2266,67 @@ def define_archive_filters_group(subparser, *, sort_by=True, first_last=True):
         mid_common_parser.set_defaults(paths=[], patterns=[])
         parser.common_options.add_common_group(mid_common_parser, '_midcommand')
 
-        subparsers = parser.add_subparsers(title='required arguments', metavar='<command>')
+        mount_epilog = process_epilog("""
+        This command mounts an archive as a FUSE filesystem. This can be useful for
+        browsing an archive or restoring individual files. Unless the ``--foreground``
+        option is given the command will run in the background until the filesystem
+        is ``umounted``.
+
+        The command ``borgfs`` provides a wrapper for ``borg mount``. This can also be
+        used in fstab entries:
+        ``/path/to/repo /mnt/point fuse.borgfs defaults,noauto 0 0``
+
+        To allow a regular user to use fstab entries, add the ``user`` option:
+        ``/path/to/repo /mnt/point fuse.borgfs defaults,noauto,user 0 0``
+
+        For mount options, see the fuse(8) manual page. Additional mount options
+        supported by borg:
+
+        - versions: when used with a repository mount, this gives a merged, versioned
+          view of the files in the archives. EXPERIMENTAL, layout may change in future.
+        - allow_damaged_files: by default damaged files (where missing chunks were
+          replaced with runs of zeros by borg check ``--repair``) are not readable and
+          return EIO (I/O error). Set this option to read such files.
+
+        The BORG_MOUNT_DATA_CACHE_ENTRIES environment variable is meant for advanced users
+        to tweak the performance. It sets the number of cached data chunks; additional
+        memory usage can be up to ~8 MiB times this number. The default is the number
+        of CPU cores.
+
+        When the daemonized process receives a signal or crashes, it does not unmount.
+        Unmounting in these cases could cause an active rsync or similar process
+        to unintentionally delete data.
+
+        When running in the foreground ^C/SIGINT unmounts cleanly, but other
+        signals or crashes do not.
+        """)
+
+        if parser.prog == 'borgfs':
+            parser.description = self.do_mount.__doc__
+            parser.epilog = mount_epilog
+            parser.formatter_class = argparse.RawDescriptionHelpFormatter
+            parser.help = 'mount repository'
+            subparser = parser
+        else:
+            subparsers = parser.add_subparsers(title='required arguments', metavar='<command>')
+            subparser = subparsers.add_parser('mount', parents=[common_parser], add_help=False,
+                                            description=self.do_mount.__doc__,
+                                            epilog=mount_epilog,
+                                            formatter_class=argparse.RawDescriptionHelpFormatter,
+                                            help='mount repository')
+        subparser.set_defaults(func=self.do_mount)
+        subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', type=location_validator(),
+                            help='repository/archive to mount')
+        subparser.add_argument('mountpoint', metavar='MOUNTPOINT', type=str,
+                            help='where to mount filesystem')
+        subparser.add_argument('-f', '--foreground', dest='foreground',
+                            action='store_true',
+                            help='stay in foreground, do not daemonize')
+        subparser.add_argument('-o', dest='options', type=str,
+                            help='Extra mount options')
+        define_archive_filters_group(subparser)
+        if parser.prog == 'borgfs':
+            return parser
 
         serve_epilog = process_epilog("""
         This command starts a repository server process. This command is usually not used manually.
@@ -3035,57 +3095,6 @@ def define_archive_filters_group(subparser, *, sort_by=True, first_last=True):
         define_archive_filters_group(subparser)
         define_exclusion_group(subparser)
 
-        mount_epilog = process_epilog("""
-        This command mounts an archive as a FUSE filesystem. This can be useful for
-        browsing an archive or restoring individual files. Unless the ``--foreground``
-        option is given the command will run in the background until the filesystem
-        is ``umounted``.
-
-        The command ``borgfs`` provides a wrapper for ``borg mount``. This can also be
-        used in fstab entries:
-        ``/path/to/repo /mnt/point fuse.borgfs defaults,noauto 0 0``
-
-        To allow a regular user to use fstab entries, add the ``user`` option:
-        ``/path/to/repo /mnt/point fuse.borgfs defaults,noauto,user 0 0``
-
-        For mount options, see the fuse(8) manual page. Additional mount options
-        supported by borg:
-
-        - versions: when used with a repository mount, this gives a merged, versioned
-          view of the files in the archives. EXPERIMENTAL, layout may change in future.
-        - allow_damaged_files: by default damaged files (where missing chunks were
-          replaced with runs of zeros by borg check ``--repair``) are not readable and
-          return EIO (I/O error). Set this option to read such files.
-
-        The BORG_MOUNT_DATA_CACHE_ENTRIES environment variable is meant for advanced users
-        to tweak the performance. It sets the number of cached data chunks; additional
-        memory usage can be up to ~8 MiB times this number. The default is the number
-        of CPU cores.
-
-        When the daemonized process receives a signal or crashes, it does not unmount.
-        Unmounting in these cases could cause an active rsync or similar process
-        to unintentionally delete data.
-
-        When running in the foreground ^C/SIGINT unmounts cleanly, but other
-        signals or crashes do not.
-        """)
-        subparser = subparsers.add_parser('mount', parents=[common_parser], add_help=False,
-                                          description=self.do_mount.__doc__,
-                                          epilog=mount_epilog,
-                                          formatter_class=argparse.RawDescriptionHelpFormatter,
-                                          help='mount repository')
-        subparser.set_defaults(func=self.do_mount)
-        subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', type=location_validator(),
-                               help='repository/archive to mount')
-        subparser.add_argument('mountpoint', metavar='MOUNTPOINT', type=str,
-                               help='where to mount filesystem')
-        subparser.add_argument('-f', '--foreground', dest='foreground',
-                               action='store_true',
-                               help='stay in foreground, do not daemonize')
-        subparser.add_argument('-o', dest='options', type=str,
-                               help='Extra mount options')
-        define_archive_filters_group(subparser)
-
         umount_epilog = process_epilog("""
         This command un-mounts a FUSE filesystem that was mounted with ``borg mount``.
 
@@ -3827,10 +3836,6 @@ def sig_trace_handler(sig_no, stack):  # pragma: no cover
 
 
 def main():  # pragma: no cover
-    # provide 'borg mount' behaviour when the main script/executable is named borgfs
-    if os.path.basename(sys.argv[0]) == "borgfs":
-        sys.argv.insert(1, "mount")
-
     # Make sure stdout and stderr have errors='replace' to avoid unicode
     # issues when print()-ing unicode file names
     sys.stdout = ErrorIgnoringTextIOWrapper(sys.stdout.buffer, sys.stdout.encoding, 'replace', line_buffering=True)


Test patch: