diff --git a/fuse.py b/fuse.py index 2b80c09..ec4a3eb 100644 --- a/fuse.py +++ b/fuse.py @@ -22,13 +22,11 @@ from errno import * from os import environ import re +import argparse +from argparse import ArgumentParser, SUPPRESS from fuseparts import __version__ from fuseparts._fuse import main, FuseGetContext, FuseInvalidate, FuseNotifyPoll from fuseparts._fuse import FuseError, FuseAPIVersion -from fuseparts.subbedopts import SubOptsHive, SubbedOptFormatter -from fuseparts.subbedopts import SubbedOptIndentedFormatter, SubbedOptParse -from fuseparts.subbedopts import SUPPRESS_HELP, OptParseError -from fuseparts.setcompatwrap import set ########## @@ -85,7 +83,7 @@ def get_compat_0_1(): -class FuseArgs(SubOptsHive): +class FuseArgs: """ Class representing a FUSE command line. """ @@ -95,11 +93,10 @@ class FuseArgs(SubOptsHive): 'foreground': '-f'} def __init__(self): - - SubOptsHive.__init__(self) - self.modifiers = {} self.mountpoint = None + self.optlist = set() + self.optdict = {} for m in self.fuse_modifiers: self.modifiers[m] = False @@ -110,6 +107,14 @@ def __str__(self): ',\n '.join(self._str_core()) + \ ' >' + def _str_core(self): + sa = [] + for k, v in self.optdict.items(): + sa.append(str(k) + '=' + str(v)) + ra = (list(self.optlist) + sa) or ["(none)"] + ra.sort() + return ra + def getmod(self, mod): return self.modifiers[mod] @@ -130,7 +135,8 @@ def assemble(self): """Mangle self into an argument array""" self.canonify() - args = [sys.argv and sys.argv[0] or "python"] + # Use the actual program name from sys.argv[0] + args = [os.path.basename(sys.argv[0]) if sys.argv else "python"] if self.mountpoint: args.append(self.mountpoint) for m, v in self.modifiers.items(): @@ -138,46 +144,83 @@ def assemble(self): args.append(self.fuse_modifiers[m]) opta = [] - for o, v in self.optdict.items(): - opta.append(o + '=' + v) - opta.extend(self.optlist) + # Add options in a consistent order + for o in sorted(self.optlist): + opta.append(o) + for o, v in sorted(self.optdict.items()): + opta.append(o + '=' + v) if opta: args.append("-o" + ",".join(opta)) return args - def filter(self, other=None): - """ - Same as for SubOptsHive, with the following difference: - if other is not specified, `Fuse.fuseoptref()` is run and its result - will be used. - """ + def canonify(self): + """Transform self to an equivalent canonical form""" + # Create a list of items to process to avoid modifying during iteration + items = list(self.optdict.items()) + for k, v in items: + if v == False: + self.optdict.pop(k) + elif v == True: + self.optdict.pop(k) + self.optlist.add(v) + else: + self.optdict[k] = str(v) + def filter(self, other=None): + """Filter options based on another FuseArgs instance""" if not other: other = Fuse.fuseoptref() - return SubOptsHive.filter(self, other) - - + self.canonify() + other.canonify() + + rej = self.__class__() + rej.optlist = self.optlist.difference(other.optlist) + self.optlist.difference_update(rej.optlist) + for x in self.optdict.copy(): + if x not in other.optdict: + self.optdict.pop(x) + rej.optdict[x] = None + + return rej + + def add(self, opt, val=None): + """Add a suboption""" + ov = opt.split('=', 1) + o = ov[0] + v = len(ov) > 1 and ov[1] or None + + if v: + if val is not None: + raise AttributeError("ambiguous option value") + val = v + + if val == False: + return + + if val in (None, True): + self.optlist.add(o) + else: + self.optdict[o] = val -class FuseFormatter(SubbedOptIndentedFormatter): - def __init__(self, **kw): - if not 'indent_increment' in kw: - kw['indent_increment'] = 4 - SubbedOptIndentedFormatter.__init__(self, **kw) +# Custom error class for FUSE-specific errors +class FuseError(Exception): + """Exception raised for FUSE-specific errors""" + pass - def store_option_strings(self, parser): - SubbedOptIndentedFormatter.store_option_strings(self, parser) - # 27 is how the lib stock help appears - self.help_position = max(self.help_position, 27) - self.help_width = self.width - self.help_position +# Custom action for the -s option +class SingleThreadAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + if parser.fuse: + parser.fuse.multithreaded = False -class FuseOptParse(SubbedOptParse): +class FuseOptParse(ArgumentParser): """ - This class alters / enhances `SubbedOptParse` so that it's + This class alters / enhances `ArgumentParser` so that it's suitable for usage with FUSE. - When adding options, you can use the `mountopt` pseudo-attribute which @@ -236,53 +279,55 @@ class FuseOptParse(SubbedOptParse): suggests the user to read this documentation. dash_o_handler - Argument should be a SubbedOpt instance (created with - ``action="store_hive"`` if you want it to be useful). - This lets you customize the handler of the ``-o`` option. For example, + Argument should be a handler for the ``-o`` option. For example, you can alter or suppress the generic ``-o`` entry in help output. """ def __init__(self, *args, **kw): self.mountopts = [] - - self.fuse_args = \ - 'fuse_args' in kw and kw.pop('fuse_args') or FuseArgs() - dsd = 'dash_s_do' in kw and kw.pop('dash_s_do') or 'whine' - if 'fetch_mp' in kw: - self.fetch_mp = bool(kw.pop('fetch_mp')) - else: - self.fetch_mp = True - if 'standard_mods' in kw: - smods = bool(kw.pop('standard_mods')) - else: - smods = True - if 'fuse' in kw: - self.fuse = kw.pop('fuse') - if not 'formatter' in kw: - kw['formatter'] = FuseFormatter() - doh = 'dash_o_handler' in kw and kw.pop('dash_o_handler') - - SubbedOptParse.__init__(self, *args, **kw) - + self.fuse_args = kw.pop('fuse_args', FuseArgs()) + dsd = kw.pop('dash_s_do', 'whine') + self.fetch_mp = kw.pop('fetch_mp', True) + smods = kw.pop('standard_mods', True) + self.fuse = kw.pop('fuse', None) + doh = kw.pop('dash_o_handler', None) + + # Handle version parameter + version = kw.pop('version', None) + if version: + kw['description'] = version + + # Call super().__init__() without version argument + super().__init__(*args, **kw) + + # Add version action after super().__init__() + if version: + self.add_argument('--version', action='version', version=version) + + # Add mountpoint argument first + if self.fetch_mp: + self.add_argument('mountpoint', nargs='?', help='Mount point') + + # Add mount options handler if doh: - self.add_option(doh) + self.add_argument(doh) else: - self.add_option('-o', action='store_hive', - subopts_hive=self.fuse_args, help="mount options", - metavar="opt,[opt...]") + self.add_argument('-o', '--options', help="mount options", + metavar="opt,[opt...]", dest='mount_options', + action='store', default='') + # Add standard FUSE modifiers if smods: - self.add_option('-f', action='callback', - callback=lambda *a: self.fuse_args.setmod('foreground'), - help=SUPPRESS_HELP) - self.add_option('-d', action='callback', - callback=lambda *a: self.fuse_args.add('debug'), - help=SUPPRESS_HELP) + self.add_argument('-f', '--foreground', action='store_true', + help=SUPPRESS, dest='foreground') + self.add_argument('-d', '--debug', action='store_true', + help=SUPPRESS, dest='debug') + # Handle -s option based on dash_s_do setting if dsd == 'whine': def dsdcb(option, opt_str, value, parser): - raise RuntimeError(""" + raise FuseError(""" ! If you want the "-s" option to work, pass ! @@ -293,53 +338,64 @@ def dsdcb(option, opt_str, value, parser): """) elif dsd == 'setsingle': - def dsdcb(option, opt_str, value, parser): - self.fuse.multithreaded = False - - elif dsd == 'undef': - dsdcb = None - else: - raise ArgumentError("key `dash_s_do': uninterpreted value " + str(dsd)) - - if dsdcb: - self.add_option('-s', action='callback', callback=dsdcb, - help=SUPPRESS_HELP) - - - def exit(self, status=0, msg=None): - if msg: - sys.stderr.write(msg) - - def error(self, msg): - SubbedOptParse.error(self, msg) - raise OptParseError(msg) - - def print_help(self, file=sys.stderr): - SubbedOptParse.print_help(self, file) - self.fuse_args.setmod('showhelp') - - def print_version(self, file=sys.stderr): - SubbedOptParse.print_version(self, file) - self.fuse_args.setmod('showversion') - - def parse_args(self, args=None, values=None): - o, a = SubbedOptParse.parse_args(self, args, values) - if a and self.fetch_mp: - self.fuse_args.mountpoint = os.path.realpath(a.pop()) - return o, a - - def add_option(self, *opts, **attrs): - if 'mountopt' in attrs: - if opts or 'subopt' in attrs: - raise OptParseError( - "having options or specifying the `subopt' attribute conflicts with `mountopt' attribute") - opts = ('-o',) - attrs['subopt'] = attrs.pop('mountopt') - if not 'dest' in attrs: - attrs['dest'] = attrs['subopt'] - - SubbedOptParse.add_option(self, *opts, **attrs) - + self.add_argument('-s', '--single', action=SingleThreadAction, + nargs=0, help=SUPPRESS) + elif dsd != 'undef': + raise ValueError("key 'dash_s_do': uninterpreted value " + str(dsd)) + + + def parse_args(self, args=None, namespace=None): + """Parse command line arguments""" + if namespace is None: + namespace = argparse.Namespace() + + # Store original args for mountpoint handling + original_args = args + + # Parse arguments + parsed_args = super().parse_args(args, namespace) + + # Handle mountpoint + if hasattr(parsed_args, 'mountpoint') and parsed_args.mountpoint and self.fetch_mp: + self.fuse_args.mountpoint = os.path.realpath(parsed_args.mountpoint) + elif original_args and len(original_args) > 0 and not original_args[0].startswith('-'): + # If no mountpoint argument was provided but we have a positional argument + # that's not an option, use it as the mountpoint + self.fuse_args.mountpoint = os.path.realpath(original_args[0]) + + # Handle modifiers + if hasattr(parsed_args, 'foreground') and parsed_args.foreground: + self.fuse_args.setmod('foreground') + if hasattr(parsed_args, 'debug') and parsed_args.debug: + self.fuse_args.add('debug') + + # Handle mount options + if hasattr(parsed_args, 'mount_options') and parsed_args.mount_options: + for opt in parsed_args.mount_options.split(','): + self.fuse_args.add(opt) + + return parsed_args + + def add_argument(self, *args, **kwargs): + """Add an argument to the parser""" + if 'mountopt' in kwargs: + if args or 'action' in kwargs: + raise ValueError( + "having options or specifying the `action' attribute conflicts with `mountopt' attribute") + kwargs['action'] = 'store' + kwargs['dest'] = kwargs.pop('mountopt') + args = ('-o', '--options') + + return super().add_argument(*args, **kwargs) + + def error(self, message): + """Handle parsing errors""" + raise FuseError(message) + + def exit(self, status=0, message=None): + """Handle parser exit""" + if message: + sys.stderr.write(message) ########## @@ -692,8 +748,7 @@ def malformed(): ! However, the latest available is """ + repr(FUSE_PYTHON_API_VERSION) + """. """) - self.fuse_args = \ - 'fuse_args' in kw and kw.pop('fuse_args') or FuseArgs() + self.fuse_args = kw.pop('fuse_args', FuseArgs()) if get_compat_0_1(): return self.__init_0_1__(*args, **kw) @@ -705,8 +760,12 @@ def malformed(): if not 'fuse_args' in kw: kw['fuse_args'] = self.fuse_args kw['fuse'] = self - parserclass = \ - 'parser_class' in kw and kw.pop('parser_class') or FuseOptParse + parserclass = kw.pop('parser_class', FuseOptParse) + + # Handle version argument before passing to parser + version = kw.pop('version', None) + if version: + kw['description'] = version self.parser = parserclass(*args, **kw) self.methproxy = self.Methproxy() @@ -714,16 +773,16 @@ def malformed(): def parse(self, *args, **kw): """Parse command line, fill `fuse_args` attribute.""" - ev = 'errex' in kw and kw.pop('errex') - if ev and not isinstance(ev, int): + exit_value = kw.pop('errex', None) + if exit_value is not None and not isinstance(exit_value, int): raise TypeError("error exit value should be an integer") try: self.cmdline = self.parser.parse_args(*args, **kw) - except OptParseError: - if ev: - sys.exit(ev) - raise + except FuseError: + if exit_value is not None: + sys.exit(exit_value) + raise return self.fuse_args diff --git a/fuseparts/subbedopts.py b/fuseparts/subbedopts.py deleted file mode 100644 index c015f12..0000000 --- a/fuseparts/subbedopts.py +++ /dev/null @@ -1,268 +0,0 @@ -# -# Copyright (C) 2006 Csaba Henk -# -# This program can be distributed under the terms of the GNU LGPL. -# See the file COPYING. -# - -from optparse import Option, OptionParser, OptParseError, OptionConflictError -from optparse import HelpFormatter, IndentedHelpFormatter, SUPPRESS_HELP -from fuseparts.setcompatwrap import set - -########## -### -### Generic suboption parsing stuff. -### -########## - - - -class SubOptsHive(object): - """ - Class for collecting unhandled suboptions. - """ - - def __init__(self): - - self.optlist = set() - self.optdict = {} - - def _str_core(self): - - sa = [] - for k, v in self.optdict.items(): - sa.append(str(k) + '=' + str(v)) - - ra = (list(self.optlist) + sa) or ["(none)"] - ra.sort() - return ra - - def __str__(self): - return "< opts: " + ", ".join(self._str_core()) + " >" - - def canonify(self): - """ - Transform self to an equivalent canonical form: - delete optdict keys with False value, move optdict keys - with True value to optlist, stringify other values. - """ - - for k, v in self.optdict.items(): - if v == False: - self.optdict.pop(k) - elif v == True: - self.optdict.pop(k) - self.optlist.add(v) - else: - self.optdict[k] = str(v) - - def filter(self, other): - """ - Throw away those options which are not in the other one. - Returns a new instance with the rejected options. - """ - - self.canonify() - other.canonify() - - rej = self.__class__() - rej.optlist = self.optlist.difference(other.optlist) - self.optlist.difference_update(rej.optlist) - for x in self.optdict.copy(): - if x not in other.optdict: - self.optdict.pop(x) - rej.optdict[x] = None - - return rej - - def add(self, opt, val=None): - """Add a suboption.""" - - ov = opt.split('=', 1) - o = ov[0] - v = len(ov) > 1 and ov[1] or None - - if (v): - if val != None: - raise AttributeError("ambiguous option value") - val = v - - if val == False: - return - - if val in (None, True): - self.optlist.add(o) - else: - self.optdict[o] = val - - - -class SubbedOpt(Option): - """ - `Option` derivative enhanced with the attribute of being a suboption of - some other option (like ``foo`` and ``bar`` for ``-o`` in ``-o foo,bar``). - """ - - ATTRS = Option.ATTRS + ["subopt", "subsep", "subopts_hive"] - ACTIONS = Option.ACTIONS + ("store_hive",) - STORE_ACTIONS = Option.STORE_ACTIONS + ("store_hive",) - TYPED_ACTIONS = Option.TYPED_ACTIONS + ("store_hive",) - - def __init__(self, *opts, **attrs): - - self.subopt_map = {} - - if "subopt" in attrs: - self._short_opts = [] - self._long_opts = [] - self._set_opt_strings(opts) - self.baseopt = self._short_opts[0] or self._long_opts[0] - opts = () - - Option.__init__(self, *opts, **attrs) - - def __str__(self): - pf = "" - if hasattr(self, "subopt") and self.subopt: - pf = " %s...,%s,..." % (self.baseopt, self.subopt) - return Option.__str__(self) + pf - - def _check_opt_strings(self, opts): - return opts - - def _check_dest(self): - try: - Option._check_dest(self) - except IndexError: - if self.subopt: - self.dest = "__%s__%s" % (self.baseopt, self.subopt) - self.dest = self.dest.replace("-", "") - else: - raise - - def get_opt_string(self): - if hasattr(self, 'subopt'): - return self.subopt - else: - return Option.get_opt_string(self) - - def take_action(self, action, dest, opt, value, values, parser): - if action == "store_hive": - if not hasattr(values, dest) or getattr(values, dest) == None: - if hasattr(self, "subopts_hive") and self.subopts_hive: - hive = self.subopts_hive - else: - hive = parser.hive_class() - setattr(values, dest, hive) - for o in value.split(self.subsep or ","): - oo = o.split('=') - ok = oo[0] - ov = None - if (len(oo) > 1): - ov = oo[1] - if ok in self.subopt_map: - self.subopt_map[ok].process(ok, ov, values, parser) - else: - getattr(values, dest).add(*oo) - return - Option.take_action(self, action, dest, opt, value, values, parser) - - def register_sub(self, o): - """Register argument a suboption for `self`.""" - - if o.subopt in self.subopt_map: - raise OptionConflictError( - "conflicting suboption handlers for `%s'" % o.subopt, - o) - self.subopt_map[o.subopt] = o - - CHECK_METHODS = [] - for m in Option.CHECK_METHODS: - #if not m == Option._check_dest: - if not m.__name__ == '_check_dest': - CHECK_METHODS.append(m) - CHECK_METHODS.append(_check_dest) - - - -class SubbedOptFormatter(HelpFormatter): - - def format_option_strings(self, option): - if hasattr(option, "subopt") and option.subopt: - res = '-o ' + option.subopt - if option.takes_value(): - res += "=" - res += option.metavar or 'FOO' - return res - - return HelpFormatter.format_option_strings(self, option) - - - -class SubbedOptIndentedFormatter(IndentedHelpFormatter, SubbedOptFormatter): - - def format_option_strings(self, option): - return SubbedOptFormatter.format_option_strings(self, option) - - - -class SubbedOptParse(OptionParser): - """ - This class alters / enhances `OptionParser` with *suboption handlers*. - - That is, calling `sop.add_option('-x', subopt=foo)` installs a handler - which will be triggered if there is ``-x foo`` in the command line being - parsed (or, eg., ``-x foo,bar``). - - Moreover, ``-x`` implicitly gets a handler which collects the unhandled - suboptions of ``-x`` into a `SubOptsHive` instance (accessible post festam - via the `x` attribute of the returned Values object). (The only exception - is when ``-x`` has *explicitly* been added with action ``store_hive``. - This opens up the possibility of customizing the ``-x`` handler at some - rate.) - - Suboption handlers have all the nice features of normal option handlers, - eg. they are displayed in the automatically generated help message - (and can have their own help info). - """ - - def __init__(self, *args, **kw): - - if not 'formatter' in kw: - kw['formatter'] = SubbedOptIndentedFormatter() - if not 'option_class' in kw: - kw['option_class'] = SubbedOpt - if 'hive_class' in kw: - self.hive_class = kw.pop('hive_class') - else: - self.hive_class = SubOptsHive - - OptionParser.__init__(self, *args, **kw) - - def add_option(self, *args, **kwargs): - if 'action' in kwargs and kwargs['action'] == 'store_hive': - if 'subopt' in kwargs: - raise OptParseError( - """option can't have a `subopt' attr and `action="store_hive"' at the same time""") - if not 'type' in kwargs: - kwargs['type'] = 'string' - elif 'subopt' in kwargs: - o = self.option_class(*args, **kwargs) - - oo = self.get_option(o.baseopt) - if oo: - if oo.action != "store_hive": - raise OptionConflictError( - "can't add subopt as option has already a handler that doesn't do `store_hive'", - oo) - else: - self.add_option(o.baseopt, action='store_hive', - metavar="sub1,[sub2,...]") - oo = self.get_option(o.baseopt) - - oo.register_sub(o) - - args = (o,) - kwargs = {} - - return OptionParser.add_option(self, *args, **kwargs) diff --git a/tests/test_fuse_args.py b/tests/test_fuse_args.py new file mode 100644 index 0000000..630f76e --- /dev/null +++ b/tests/test_fuse_args.py @@ -0,0 +1,153 @@ +import pytest +import optparse +import os +import sys +from pathlib import Path +import argparse + +from fuse import FuseArgs, FuseOptParse, Fuse, FuseError, FUSE_PYTHON_API_VERSION + +# Set up fuse_python_api for testing +import fuse +fuse.fuse_python_api = FUSE_PYTHON_API_VERSION + + +def test_init(): + """FuseArgs initialization""" + args = FuseArgs() + assert args.mountpoint is None + assert args.optlist == set() + assert args.optdict == {} + assert args.getmod('showhelp') is False + assert args.getmod('showversion') is False + assert args.getmod('foreground') is False + +@pytest.mark.parametrize("mod", ['showhelp', 'showversion', 'foreground']) +def test_get_set_mod(mod): + """FuseArgs modifier handling""" + args = FuseArgs() + args.setmod(mod) + assert args.getmod(mod) is True + + args.unsetmod(mod) + assert args.getmod(mod) is False + +def test_fuse_args_mount_expected(): + """FuseArgs mount_expected method""" + args = FuseArgs() + assert args.mount_expected() is True + + args.setmod('showhelp') + assert args.mount_expected() is False + args.unsetmod('showhelp') + + args.setmod('showversion') + assert args.mount_expected() is False + args.unsetmod('showversion') + +def test_fuse_args_add(): + """FuseArgs add method""" + args = FuseArgs() + + # Test adding simple option + args.add('debug') + assert 'debug' in args.optlist + + # Test adding option with value + args.add('uid=1000') + assert args.optdict['uid'] == '1000' + + # Test adding option with explicit value + args.add('gid', '1000') + assert args.optdict['gid'] == '1000' + + # Test adding False value + args.add('allow_other', False) + assert 'allow_other' not in args.optlist + assert 'allow_other' not in args.optdict + +def test_fuse_args_canonify(): + """FuseArgs canonify method""" + args = FuseArgs() + + # Test converting False values + args.optdict['allow_other'] = False + args.canonify() + assert 'allow_other' not in args.optdict + + # Test converting True values + args.optdict['debug'] = True + args.canonify() + assert True in args.optlist + assert True not in args.optdict + +def test_fuse_args_assemble(): + """FuseArgs assemble method""" + args = FuseArgs() + args.mountpoint = '/mnt' + args.setmod('foreground') + args.add('debug') + args.add('uid=1000') + + cmdline = args.assemble() + # The first argument should be a program name + assert isinstance(cmdline[0], str) + assert cmdline[1] == '/mnt' + assert '-f' in cmdline + assert cmdline[-1] in ('-odebug,uid=1000', '-ouid=1000,debug') + +def test_fuse_cliparse_basic(): + """Basic FuseOptParse functionality""" + parser = FuseOptParse() + parser.add_argument('--mountpoint', nargs='?', help='Mount point') + args = parser.parse_args(['/mnt', '-f', '-o', 'debug,uid=1000']) + + assert parser.fuse_args.mountpoint == '/mnt' + assert parser.fuse_args.getmod('foreground') + assert 'debug' in parser.fuse_args.optlist + assert parser.fuse_args.optdict['uid'] == '1000' + +def test_fuse_cliparse_standard_mods(): + """FuseOptParse standard modifiers""" + parser = FuseOptParse(standard_mods=True) + parser.add_argument('--mountpoint', nargs='?', help='Mount point') + args = parser.parse_args(['/mnt', '-f', '-d']) + + assert parser.fuse_args.getmod('foreground') + assert 'debug' in parser.fuse_args.optlist + +def test_fuse_cliparse_dash_s(): + """FuseOptParse -s option handling""" + # Test whine mode (default) + parser = FuseOptParse() + parser.add_argument('--mountpoint', nargs='?', help='Mount point') + with pytest.raises(FuseError): + parser.parse_args(['/mnt', '-s']) + + # Test setsingle mode + mock_fuse = Fuse() + mock_fuse.multithreaded = True + + parser = FuseOptParse(dash_s_do='setsingle', fuse=mock_fuse) + parser.add_argument('--mountpoint', nargs='?', help='Mount point') + args = parser.parse_args(['/mnt', '-s']) + assert not mock_fuse.multithreaded + +def test_fuse_cliparse_error_handling(): + """FuseOptParse error handling""" + parser = FuseOptParse() + parser.add_argument('--mountpoint', nargs='?', help='Mount point') + + # Test invalid option + with pytest.raises(FuseError): + parser.parse_args(['/mnt', '--invalid-option']) + +def test_fuse_cliparse_mount_options(): + """FuseOptParse mount options handling""" + parser = FuseOptParse() + parser.add_argument('--mountpoint', nargs='?', help='Mount point') + args = parser.parse_args(['/mnt', '-o', 'debug,uid=1000,gid=1000']) + + assert 'debug' in parser.fuse_args.optlist + assert parser.fuse_args.optdict['uid'] == '1000' + assert parser.fuse_args.optdict['gid'] == '1000'