Re-factor the option parsing following a suggestion from Greg.
Eliminate the <topic>.parse altogether.

Signed-off-by: Jean-Marc Eurin <[email protected]>


git-svn-id: http://test.kernel.org/svn/autotest/trunk@3081 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/cli/acl.py b/cli/acl.py
index b5832fd..a6a01df 100755
--- a/cli/acl.py
+++ b/cli/acl.py
@@ -38,14 +38,10 @@
                                default=None,
                                metavar='ACL_FLIST')
 
-
-    def parse(self, flists=None, req_items='acls'):
-        """Consume the common acl options"""
-        if flists:
-            flists.append(('acls', 'alist', '', True))
-        else:
-            flists = [('acls', 'alist', '', True)]
-        return self.parse_with_flist(flists, req_items)
+        self.topic_parse_info = topic_common.item_parse_info(
+            attribute_name='acls',
+            filename_option='alist',
+            use_leftover=True)
 
 
     def get_items(self):
@@ -75,10 +71,13 @@
 
 
     def parse(self):
-        flists = [('users', '', 'user', False),
-                  ('hosts', '', 'machine', False)]
-        (options, leftover) = super(acl_list, self).parse(flists,
-                                                          req_items=None)
+        user_info = topic_common.item_parse_info(attribute_name='users',
+                                                 inline_option='user')
+        host_info = topic_common.item_parse_info(attribute_name='hosts',
+                                                 inline_option='machine')
+
+        (options, leftover) = super(acl_list, self).parse([user_info,
+                                                           host_info])
 
         if ((self.users and (self.hosts or self.acls)) or
             (self.hosts and self.acls)):
@@ -140,7 +139,7 @@
 
 
     def parse(self):
-        (options, leftover) = super(acl_create, self).parse()
+        (options, leftover) = super(acl_create, self).parse(req_items='acls')
 
         if not options.desc:
             self.invalid_syntax('Must specify a description to create an ACL.')
@@ -187,10 +186,15 @@
 
 
     def parse(self):
-        flists = [('users', 'ulist', 'user', False),
-                  ('hosts', 'mlist', 'machine', False)]
-
-        (options, leftover) = super(acl_add_or_remove, self).parse(flists)
+        user_info = topic_common.item_parse_info(attribute_name='users',
+                                                 inline_option='user',
+                                                 filename_option='ulist')
+        host_info = topic_common.item_parse_info(attribute_name='hosts',
+                                                 inline_option='machine',
+                                                 filename_option='mlist')
+        (options, leftover) = super(acl_add_or_remove,
+                                    self).parse([user_info, host_info],
+                                                req_items='acls')
 
         if (not getattr(self, 'users', None) and
             not getattr(self, 'hosts', None)):
diff --git a/cli/atomicgroup.py b/cli/atomicgroup.py
index 429569b..8384d13 100644
--- a/cli/atomicgroup.py
+++ b/cli/atomicgroup.py
@@ -35,12 +35,10 @@
                                type='string', default=None,
                                metavar='ATOMIC_GROUP_FLIST')
 
-
-    def parse(self, flists=None, req_items='atomicgroups'):
-        if not flists:
-            flists = []
-        flists.append(('atomicgroups', 'glist', '', True))
-        return self.parse_with_flist(flists, req_items)
+        self.topic_parse_info = topic_common.item_parse_info(
+            attribute_name='atomicgroups',
+            filename_option='glist',
+            use_leftover=True)
 
 
     def get_items(self):
@@ -62,7 +60,7 @@
 
 
     def parse(self):
-        options, leftover = super(atomicgroup_list, self).parse(req_items=None)
+        options, leftover = super(atomicgroup_list, self).parse()
         self.show_invalid = options.show_invalid
         return options, leftover
 
@@ -125,8 +123,13 @@
 
 
     def parse(self):
-        flists = [('labels', 'label_list', 'label', False)]
-        options, leftover = super(atomicgroup_add_or_remove, self).parse(flists)
+        label_info = topic_common.item_parse_info(attribute_name='labels',
+                                                  inline_option='label',
+                                                  filename_option='label_list')
+
+        options, leftover = super(atomicgroup_add_or_remove,
+                                  self).parse([label_info],
+                                              req_items='atomicgroups')
         if not getattr(self, 'labels', None):
             self.invalid_syntax('%s %s requires at least one label' %
                                 (self.msg_topic,
diff --git a/cli/cli_mock.py b/cli/cli_mock.py
index c616bee..27b16ac 100755
--- a/cli/cli_mock.py
+++ b/cli/cli_mock.py
@@ -26,12 +26,14 @@
 
 class cli_unittest(unittest.TestCase):
     def setUp(self):
+        super(cli_unittest, self).setUp()
         self.god = mock.mock_god(debug=CLI_UT_DEBUG)
         self.god.stub_class_method(rpc.afe_comm, 'run')
         self.god.stub_function(sys, 'exit')
 
 
     def tearDown(self):
+        super(cli_unittest, self).tearDown()
         self.god.unstub_all()
 
 
diff --git a/cli/host.py b/cli/host.py
index 35bee49..94c2016 100755
--- a/cli/host.py
+++ b/cli/host.py
@@ -47,14 +47,10 @@
                                default=None,
                                metavar='MACHINE_FLIST')
 
-
-    def parse(self, flists=None, req_items='hosts'):
-        """Consume the common host options"""
-        if flists:
-            flists.append(('hosts', 'mlist', '', True))
-        else:
-            flists = [('hosts', 'mlist', '', True)]
-        return self.parse_with_flist(flists, req_items)
+        self.topic_parse_info = topic_common.item_parse_info(
+            attribute_name='hosts',
+            filename_option='mlist',
+            use_leftover=True)
 
 
     def _parse_lock_options(self, options):
@@ -132,7 +128,7 @@
 
     def parse(self):
         """Consume the specific options"""
-        (options, leftover) = super(host_list, self).parse(req_items=None)
+        (options, leftover) = super(host_list, self).parse()
         self.labels = options.label
         self.status = options.status
         self.acl = options.acl
@@ -263,7 +259,7 @@
 
     def parse(self):
         """Consume the specific options"""
-        (options, leftover) = super(host_jobs, self).parse(req_items=None)
+        (options, leftover) = super(host_jobs, self).parse()
         self.max_queries = options.max_query
         return (options, leftover)
 
@@ -416,9 +412,16 @@
 
 
     def parse(self):
-        flists = [('labels', 'blist', 'labels', False),
-                  ('acls', 'alist', 'acls', False)]
-        (options, leftover) = super(host_create, self).parse(flists)
+        label_info = topic_common.item_parse_info(attribute_name='labels',
+                                                 inline_option='labels',
+                                                 filename_option='blist')
+        acl_info = topic_common.item_parse_info(attribute_name='acls',
+                                                inline_option='acls',
+                                                filename_option='alist')
+
+        (options, leftover) = super(host_create, self).parse([label_info,
+                                                              acl_info],
+                                                             req_items='hosts')
 
         self._parse_lock_options(options)
         self.locked = options.lock
diff --git a/cli/job.py b/cli/job.py
index 506cc15..ca6aaf5 100755
--- a/cli/job.py
+++ b/cli/job.py
@@ -44,6 +44,14 @@
 
 
 class job_list_stat(action_common.atest_list, job):
+    def __init__(self):
+        super(job_list_stat, self).__init__()
+
+        self.topic_parse_info = topic_common.item_parse_info(
+            attribute_name='jobs',
+            use_leftover=True)
+
+
     def __split_jobs_between_ids_names(self):
         job_ids = []
         job_names = []
@@ -95,8 +103,7 @@
 
 
     def parse(self):
-        (options, leftover) = self.parse_with_flist([('jobs', '', '', True)],
-                                                    None)
+        options, leftover = super(job_list, self).parse()
         self.all = options.all
         self.data['running'] = options.running
         if options.user:
@@ -107,7 +114,7 @@
         elif not options.all and not self.jobs:
             self.data['owner'] = getpass.getuser()
 
-        return (options, leftover)
+        return options, leftover
 
 
     def execute(self):
@@ -136,15 +143,13 @@
 
 
     def parse(self):
-        (options, leftover) = self.parse_with_flist(flists=[('jobs', '', '',
-                                                             True)],
-                                                    req_items='jobs')
+        options, leftover = super(job_stat, self).parse(req_items='jobs')
         if not self.jobs:
             self.invalid_syntax('Must specify at least one job.')
 
         self.show_control_file = options.control_file
 
-        return (options, leftover)
+        return options, leftover
 
 
     def _merge_results(self, summary, qes):
@@ -281,10 +286,17 @@
 
 
     def parse(self):
-        flists = [('hosts', 'mlist', 'machine', False),
-                  ('jobname', '', '', True)]
-        (options, leftover) = self.parse_with_flist(flists,
-                                                    req_items='jobname')
+        host_info = topic_common.item_parse_info(attribute_name='hosts',
+                                                 inline_option='machine',
+                                                 filename_option='mlist')
+        job_info = topic_common.item_parse_info(attribute_name='jobname',
+                                                use_leftover=True)
+        oth_info = topic_common.item_parse_info(attribute_name='one_time_hosts',
+                                                inline_option='one_time_hosts')
+
+        options, leftover = super(job_create,
+                                  self).parse([host_info, job_info, oth_info],
+                                              req_items='jobname')
         self.data = {}
         if len(self.jobname) > 1:
             self.invalid_syntax('Too many arguments specified, only expected '
@@ -299,9 +311,9 @@
             self.op_action = 'clone'
             self.msg_items = 'jobid'
             self.reuse_hosts = options.reuse_hosts
-            return (options, leftover)
+            return options, leftover
 
-        if (len(self.hosts) == 0 and not options.one_time_hosts
+        if (len(self.hosts) == 0 and not self.one_time_hosts
             and not options.labels and not options.atomic_group):
             self.invalid_syntax('Must specify at least one machine '
                                 'or an atomic group '
@@ -355,9 +367,8 @@
         if options.timeout:
             self.data['timeout'] = options.timeout
 
-        if options.one_time_hosts:
-            one_time_hosts = self._file_list(options, opt_list='one_time_hosts')
-            self.data['one_time_hosts'] = one_time_hosts
+        if self.one_time_hosts:
+            self.data['one_time_hosts'] = self.one_time_hosts
         if options.labels:
             labels = options.labels.split(',')
             labels = [label.strip() for label in labels if label.strip()]
@@ -386,7 +397,7 @@
         else:
             self.data['control_type'] = 'Client'
 
-        return (options, leftover)
+        return options, leftover
 
 
     def execute(self):
@@ -473,8 +484,10 @@
     msg_done = 'Aborted'
 
     def parse(self):
-        (options, leftover) = self.parse_with_flist([('jobids', '', '', True)],
-                                                    req_items='jobids')
+        job_info = topic_common.item_parse_info(attribute_name='jobids',
+                                                use_leftover=True)
+        options, leftover = super(job_abort, self).parse([job_info],
+                                                         req_items='jobids')
 
 
     def execute(self):
diff --git a/cli/label.py b/cli/label.py
index a4d6ac2..244f795 100755
--- a/cli/label.py
+++ b/cli/label.py
@@ -38,14 +38,10 @@
                                default=None,
                                metavar='LABEL_FLIST')
 
-
-    def parse(self, flists=None, req_items='labels'):
-        """Consume the common label options"""
-        if flists:
-            flists.append(('labels', 'blist', '', True))
-        else:
-            flists = [('labels', 'blist', '', True)]
-        return self.parse_with_flist(flists, req_items)
+        self.topic_parse_info = topic_common.item_parse_info(
+            attribute_name='labels',
+            filename_option='blist',
+            use_leftover=True)
 
 
     def get_items(self):
@@ -85,9 +81,9 @@
 
 
     def parse(self):
-        flists = [('hosts', '', 'machine', False)]
-        (options, leftover) = super(label_list, self).parse(flists,
-                                                            req_items=None)
+        host_info = topic_common.item_parse_info(attribute_name='hosts',
+                                                 inline_option='machine')
+        (options, leftover) = super(label_list, self).parse([host_info])
 
         if options.all and options.platform_only:
             self.invalid_syntax('Only specify one of --all,'
@@ -154,7 +150,8 @@
 
 
     def parse(self):
-        (options, leftover) = super(label_create, self).parse()
+        (options, leftover) = super(label_create,
+                                    self).parse(req_items='labels')
         self.data_item_key = 'name'
         self.data['platform'] = options.platform
         self.data['only_if_needed'] = options.only_if_needed
@@ -184,8 +181,13 @@
 
 
     def parse(self):
-        flists = [('hosts', 'mlist', 'machine', False)]
-        (options, leftover) = super(label_add_or_remove, self).parse(flists)
+        host_info = topic_common.item_parse_info(attribute_name='hosts',
+                                                 inline_option='machine',
+                                                 filename_option='mlist')
+        (options, leftover) = super(label_add_or_remove,
+                                    self).parse([host_info],
+                                                req_items='labels')
+
         if not getattr(self, 'hosts', None):
             self.invalid_syntax('%s %s requires at least one host' %
                                 (self.msg_topic,
diff --git a/cli/test.py b/cli/test.py
index 5767889..ac1605a 100755
--- a/cli/test.py
+++ b/cli/test.py
@@ -37,10 +37,10 @@
                                default=None,
                                metavar='TEST_FLIST')
 
-
-    def parse(self, req_items=None):
-        """Consume the common test options"""
-        return self.parse_with_flist([('tests', 'blist', '', True)], req_items)
+        self.topic_parse_info = topic_common.item_parse_info(
+            attribute_name='tests',
+            filename_option='tlist',
+            use_leftover=True)
 
 
     def get_items(self):
diff --git a/cli/topic_common.py b/cli/topic_common.py
index 7167525..bf47ce3 100755
--- a/cli/topic_common.py
+++ b/cli/topic_common.py
@@ -135,6 +135,67 @@
     pass
 
 
+class item_parse_info(object):
+    def __init__(self, attribute_name, inline_option='',
+                 filename_option='', use_leftover=False):
+        """Object keeping track of the parsing options that will
+        make up the content of the atest attribute:
+        atttribute_name: the atest attribute name to populate    (label)
+        inline_option: the option containing the items           (--label)
+        filename_option: the option containing the filename      (--blist)
+        use_leftover: whether to add the leftover arguments or not."""
+        self.attribute_name = attribute_name
+        self.filename_option = filename_option
+        self.inline_option = inline_option
+        self.use_leftover = use_leftover
+
+
+    def get_values(self, options, leftover=[]):
+        """Returns the value for that attribute by accumualting all
+        the values found through the inline option, the parsing of the
+        file and the leftover"""
+        def __get_items(string, split_re='[\s,]\s*'):
+            return (item.strip() for item in re.split(split_re, string)
+                    if item)
+
+        if self.use_leftover:
+            add_on = leftover
+            leftover = []
+        else:
+            add_on = []
+
+        # Start with the add_on
+        result = set()
+        for items in add_on:
+            # Don't split on space here because the add-on
+            # may have some spaces (like the job name)
+            result.update(__get_items(items, split_re='[,]'))
+
+        # Process the inline_option, if any
+        try:
+            items = getattr(options, self.inline_option)
+            result.update(__get_items(items))
+        except (AttributeError, TypeError):
+            pass
+
+        # Process the file list, if any and not empty
+        # The file can contain space and/or comma separated items
+        try:
+            flist = getattr(options, self.filename_option)
+            file_content = []
+            for line in open(flist).readlines():
+                file_content += __get_items(line)
+            if len(file_content) == 0:
+                raise CliError("Empty file %s" % flist)
+            result.update(file_content)
+        except (AttributeError, TypeError):
+            pass
+        except IOError:
+            raise CliError("Could not open file %s" % flist)
+
+        return list(result), leftover
+
+
 class atest(object):
     """Common class for generic processing
     Should only be instantiated by itself for usage
@@ -255,6 +316,7 @@
         self.kill_on_failure = False
         self.web_server = ''
         self.verbose = False
+        self.topic_parse_info = item_parse_info(attribute_name='not_used')
 
         self.parser = optparse.OptionParser(self._get_usage())
         self.parser.add_option('-g', '--debug',
@@ -279,82 +341,30 @@
                                dest='web_server', default=None)
 
 
-    def _file_list(self, options, opt_file='', opt_list='', add_on=[]):
-        """Returns a list containing the unique items from the
-        options.<opt_list>, from the file options.<opt_file>,
-        and from the space separated add_on strings.
-        The opt_list can be space or comma separated list.
-        Used for host, acls, labels... arguments"""
-        def __get_items(string, split_on='[\s,]\s*'):
-            return [item.strip() for item in re.split(split_on, string)
-                    if item]
-
-        # Start with the add_on
-        result = set()
-        for items in add_on:
-            # Don't split on space here because the add-on
-            # may have some spaces (like the job name)
-            #result.update(item.strip() for item in items.split(',') if item)
-            result.update(__get_items(items, split_on='[,]'))
-
-        # Process the opt_list, if any
-        try:
-            items = getattr(options, opt_list)
-            result.update(__get_items(items))
-        except (AttributeError, TypeError):
-            pass
-
-        # Process the file list, if any and not empty
-        # The file can contain space and/or comma separated items
-        try:
-            flist = getattr(options, opt_file)
-            file_content = []
-            for line in open(flist).readlines():
-                file_content += __get_items(line)
-            if len(file_content) == 0:
-                self.invalid_syntax("Empty file %s" % flist)
-            result.update(file_content)
-        except (AttributeError, TypeError):
-            pass
-        except IOError:
-            self.invalid_syntax("Could not open file %s" % flist)
-
-        return list(result)
-
-
     def _get_usage(self):
         return "atest %s %s [options] %s" % (self.msg_topic.lower(),
                                              self.usage_action,
                                              self.msg_items)
 
 
-    def parse_with_flist(self, flists, req_items):
-        """Flists is a list of tuples containing:
-        (attribute, opt_fname, opt_list, use_leftover)
+    def parse(self, parse_info=[], req_items=None):
+        """parse_info is a list of item_parse_info objects
 
-        self.<atttribute> will be populated with a set
-        containing the lines of the file named
-        options.<opt_fname> and the options.<opt_list> values
-        and the leftover from the parsing if use_leftover is
-        True.  There should only be one use_leftover set to
-        True in the list.
-        Also check if the req_items is not empty after parsing."""
-        (options, leftover) = atest.parse(self)
-        if leftover is None:
-            leftover = []
+        There should only be one use_leftover set to True in the list.
 
-        for (attribute, opt_fname, opt_list, use_leftover) in flists:
-            if use_leftover:
-                add_on = leftover
-                leftover = []
-            else:
-                add_on = []
+        Also check that the req_items is not empty after parsing."""
+        (options, leftover) = self.parse_global()
 
-            setattr(self, attribute,
-                    self._file_list(options,
-                                    opt_file=opt_fname,
-                                    opt_list=opt_list,
-                                    add_on=add_on))
+        all_parse_info = parse_info[:]
+        all_parse_info.append(self.topic_parse_info)
+
+        try:
+            for item_parse_info in all_parse_info:
+                values, leftover = item_parse_info.get_values(options,
+                                                              leftover)
+                setattr(self, item_parse_info.attribute_name, values)
+        except CliError, s:
+            self.invalid_syntax(s)
 
         if (req_items and not getattr(self, req_items, None)):
             self.invalid_syntax('%s %s requires at least one %s' %
@@ -365,8 +375,8 @@
         return (options, leftover)
 
 
-    def parse(self):
-        """Parse all the arguments.
+    def parse_global(self):
+        """Parse the global arguments.
 
         It consumes what the common object needs to know, and
         let the children look at all the options.  We could
diff --git a/cli/topic_common_unittest.py b/cli/topic_common_unittest.py
index 99b516d..972f22d 100755
--- a/cli/topic_common_unittest.py
+++ b/cli/topic_common_unittest.py
@@ -10,9 +10,227 @@
 from autotest_lib.cli import cli_mock, topic_common, rpc
 from autotest_lib.frontend.afe.json_rpc import proxy
 
-class topic_common_unittest(cli_mock.cli_unittest):
+
+class item_parse_info_unittest(cli_mock.cli_unittest):
+    def __test_parsing_flist_bad(self, options):
+        parse_info = topic_common.item_parse_info
+        test_parse_info = parse_info(attribute_name='testing',
+                                     filename_option='flist')
+        self.assertRaises(topic_common.CliError,
+                          test_parse_info.get_values, options, [])
+
+
+    def __test_parsing_flist_good(self, options, expected):
+        parse_info = topic_common.item_parse_info
+        test_parse_info = parse_info(attribute_name='testing',
+                                     filename_option='flist')
+        result, leftover = test_parse_info.get_values(options, [])
+
+        self.assertEqualNoOrder(expected, result)
+        os.unlink(options.flist)
+
+
+    def __test_parsing_inline_good(self, options, expected):
+        parse_info = topic_common.item_parse_info
+        test_parse_info = parse_info(attribute_name='testing',
+                                     inline_option='inline')
+        result, leftover = test_parse_info.get_values(options, [])
+
+        self.assertEqualNoOrder(expected, result)
+
+
+    def __test_parsing_leftover_good(self, leftover, expected):
+        class opt(object):
+            pass
+        parse_info = topic_common.item_parse_info
+        test_parse_info = parse_info(attribute_name='testing',
+                                     inline_option='inline',
+                                     use_leftover=True)
+        result, leftover = test_parse_info.get_values(opt(), leftover)
+
+        self.assertEqualNoOrder(expected, result)
+
+
+    def __test_parsing_all_good(self, options, leftover, expected):
+        parse_info = topic_common.item_parse_info
+        test_parse_info = parse_info(attribute_name='testing',
+                                     inline_option='inline',
+                                     filename_option='flist',
+                                     use_leftover=True)
+        result, leftover = test_parse_info.get_values(options, leftover)
+
+        self.assertEqualNoOrder(expected, result)
+        os.unlink(options.flist)
+
+
+    def __test_parsing_all_bad(self, options, leftover):
+        parse_info = topic_common.item_parse_info
+        test_parse_info = parse_info(attribute_name='testing',
+                                     inline_option='inline',
+                                     filename_option='flist',
+                                     use_leftover=True)
+        self.assertRaises(topic_common.CliError,
+                          test_parse_info.get_values, options, leftover)
+
+
+    def test_file_list_wrong_file(self):
+        class opt(object):
+            flist = './does_not_exist'
+        self.__test_parsing_flist_bad(opt())
+
+
+    def test_file_list_empty_file(self):
+        class opt(object):
+            flist = cli_mock.create_file('')
+        self.__test_parsing_flist_bad(opt())
+
+
+    def test_file_list_ok(self):
+        class opt(object):
+            flist = cli_mock.create_file('a\nb\nc\n')
+        self.__test_parsing_flist_good(opt(), ['a', 'b', 'c'])
+
+
+    def test_file_list_one_line_space(self):
+        class opt(object):
+            flist = cli_mock.create_file('a b c\nd e\nf\n')
+        self.__test_parsing_flist_good(opt(), ['a', 'b', 'c', 'd', 'e', 'f'])
+
+
+    def test_file_list_one_line_comma(self):
+        class opt(object):
+            flist = cli_mock.create_file('a,b,c\nd,e\nf\n')
+        self.__test_parsing_flist_good(opt(), ['a', 'b', 'c', 'd', 'e', 'f'])
+
+
+    def test_file_list_one_line_mix(self):
+        class opt(object):
+            flist = cli_mock.create_file('a,b c\nd,e\nf\ng h,i')
+        self.__test_parsing_flist_good(opt(), ['a', 'b', 'c', 'd', 'e',
+                                         'f', 'g', 'h', 'i'])
+
+
+    def test_file_list_one_line_comma_space(self):
+        class opt(object):
+            flist = cli_mock.create_file('a, b c\nd,e\nf\ng h,i')
+        self.__test_parsing_flist_good(opt(), ['a', 'b', 'c', 'd', 'e',
+                                         'f', 'g', 'h', 'i'])
+
+
+    def test_file_list_line_end_comma_space(self):
+        class opt(object):
+            flist = cli_mock.create_file('a, b c\nd,e, \nf,\ng h,i ,')
+        self.__test_parsing_flist_good(opt(), ['a', 'b', 'c', 'd', 'e',
+                                         'f', 'g', 'h', 'i'])
+
+
+    def test_file_list_no_eof(self):
+        class opt(object):
+            flist = cli_mock.create_file('a\nb\nc')
+        self.__test_parsing_flist_good(opt(), ['a', 'b', 'c'])
+
+
+    def test_file_list_blank_line(self):
+        class opt(object):
+            flist = cli_mock.create_file('\na\nb\n\nc\n')
+        self.__test_parsing_flist_good(opt(), ['a', 'b', 'c'])
+
+
+    def test_file_list_opt_list_one(self):
+        class opt(object):
+            inline = 'a'
+        self.__test_parsing_inline_good(opt(), ['a'])
+
+
+    def test_file_list_opt_list_space(self):
+        class opt(object):
+            inline = 'a b c'
+        self.__test_parsing_inline_good(opt(), ['a', 'b', 'c'])
+
+
+    def test_file_list_opt_list_mix_space_comma(self):
+        class opt(object):
+            inline = 'a b,c,d e'
+        self.__test_parsing_inline_good(opt(), ['a', 'b', 'c', 'd', 'e'])
+
+
+    def test_file_list_opt_list_mix_comma_space(self):
+        class opt(object):
+            inline = 'a b,c, d e'
+        self.__test_parsing_inline_good(opt(), ['a', 'b', 'c', 'd', 'e'])
+
+
+    def test_file_list_opt_list_end_comma_space(self):
+        class opt(object):
+            inline = 'a b, ,c,, d e, '
+        self.__test_parsing_inline_good(opt(), ['a', 'b', 'c', 'd', 'e'])
+
+
+    def test_file_list_add_on_space(self):
+        self.__test_parsing_leftover_good(['a','c','b'],
+                                          ['a', 'b', 'c'])
+
+
+    def test_file_list_add_on_mix_space_comma(self):
+        self.__test_parsing_leftover_good(['a', 'c','b,d'],
+                                          ['a', 'b', 'c', 'd'])
+
+
+    def test_file_list_add_on_mix_comma_space(self):
+        self.__test_parsing_leftover_good(['a', 'c', 'b,', 'd'],
+                                          ['a', 'b', 'c', 'd'])
+
+
+    def test_file_list_add_on_end_comma_space(self):
+        self.__test_parsing_leftover_good(['a', 'c', 'b,', 'd,', ','],
+                                          ['a', 'b', 'c', 'd'])
+
+
+    def test_file_list_all_opt(self):
+        class opt(object):
+            flist = cli_mock.create_file('f\ng\nh\n')
+            inline = 'a b,c,d e'
+        self.__test_parsing_all_good(opt(), ['i', 'j'],
+                                     ['a', 'b', 'c', 'd', 'e',
+                                      'f', 'g', 'h', 'i', 'j'])
+
+
+    def test_file_list_all_opt_empty_file(self):
+        class opt(object):
+            flist = cli_mock.create_file('')
+            inline = 'a b,c,d e'
+        self.__test_parsing_all_bad(opt(), ['i', 'j'])
+
+
+    def test_file_list_all_opt_in_common(self):
+        class opt(object):
+            flist = cli_mock.create_file('f\nc\na\n')
+            inline = 'a b,c,d e'
+        self.__test_parsing_all_good(opt(), ['i','j,d'],
+                                     ['a', 'b', 'c', 'd', 'e', 'f', 'i', 'j'])
+
+
+    def test_file_list_all_opt_in_common_space(self):
+        class opt(object):
+            flist = cli_mock.create_file('a b c\nd,e\nf\ng')
+            inline = 'a b,c,d h'
+        self.__test_parsing_all_good(opt(), ['i','j,d'],
+                                     ['a', 'b', 'c', 'd', 'e',
+                                      'f', 'g', 'h', 'i', 'j'])
+
+
+    def test_file_list_all_opt_in_common_weird(self):
+        class opt(object):
+            flist = cli_mock.create_file('a b c\nd,e\nf\ng, \n, ,,')
+            inline = 'a b,c,d h, ,  ,,	'
+        self.__test_parsing_all_good(opt(), ['i','j,d'],
+                                     ['a', 'b', 'c', 'd', 'e',
+                                      'f', 'g', 'h', 'i', 'j'])
+
+
+class atest_unittest(cli_mock.cli_unittest):
     def setUp(self):
-        super(topic_common_unittest, self).setUp()
+        super(atest_unittest, self).setUp()
         self.atest = topic_common.atest()
         self.atest.afe = rpc.afe_comm()
         if 'AUTOTEST_WEB' in os.environ:
@@ -21,270 +239,7 @@
 
     def tearDown(self):
         self.atest = None
-        super(topic_common_unittest, self).tearDown()
-
-
-    def test_file_list_wrong_file(self):
-        self.god.mock_io()
-        class opt(object):
-            mlist = './does_not_exist'
-        options = opt()
-        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
-        self.assertRaises(cli_mock.ExitException,
-                          self.atest._file_list, options, opt_file='mlist')
-        self.god.check_playback()
-        (output, err) = self.god.unmock_io()
-        self.assert_(err.find('./does_not_exist') >= 0)
-
-
-    def test_file_list_empty_file(self):
-        self.god.mock_io()
-        class opt(object):
-            flist = cli_mock.create_file('')
-        options = opt()
-        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
-        self.assertRaises(cli_mock.ExitException,
-                          self.atest._file_list, options, opt_file='flist')
-        self.god.check_playback()
-        (output, err) = self.god.unmock_io()
-        self.assert_(err.find(options.flist) >= 0)
-
-
-    def test_file_list_ok(self):
-        class opt(object):
-            filename = cli_mock.create_file('a\nb\nc\n')
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c'],
-                                self.atest._file_list(options,
-                                                      opt_file='filename'))
-        os.unlink(options.filename)
-
-
-    def test_file_list_one_line_space(self):
-        class opt(object):
-            filename = cli_mock.create_file('a b c\nd e\nf\n')
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e', 'f'],
-                                self.atest._file_list(options,
-                                                      opt_file='filename'))
-        os.unlink(options.filename)
-
-
-    def test_file_list_one_line_comma(self):
-        class opt(object):
-            filename = cli_mock.create_file('a,b,c\nd,e\nf\n')
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e', 'f'],
-                                self.atest._file_list(options,
-                                                      opt_file='filename'))
-        os.unlink(options.filename)
-
-
-    def test_file_list_one_line_mix(self):
-        class opt(object):
-            filename = cli_mock.create_file('a,b c\nd,e\nf\ng h,i')
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e',
-                                 'f', 'g', 'h', 'i'],
-                                self.atest._file_list(options,
-                                                      opt_file='filename'))
-        os.unlink(options.filename)
-
-
-    def test_file_list_one_line_comma_space(self):
-        class opt(object):
-            filename = cli_mock.create_file('a, b c\nd,e\nf\ng h,i')
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e',
-                                 'f', 'g', 'h', 'i'],
-                                self.atest._file_list(options,
-                                                      opt_file='filename'))
-        os.unlink(options.filename)
-
-
-    def test_file_list_line_end_comma_space(self):
-        class opt(object):
-            filename = cli_mock.create_file('a, b c\nd,e, \nf,\ng h,i ,')
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e',
-                                 'f', 'g', 'h', 'i'],
-                                self.atest._file_list(options,
-                                                      opt_file='filename'))
-        os.unlink(options.filename)
-
-
-    def test_file_list_no_eof(self):
-        class opt(object):
-            filename = cli_mock.create_file('a\nb\nc')
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c'],
-                                self.atest._file_list(options,
-                                                      opt_file='filename'))
-        os.unlink(options.filename)
-
-
-    def test_file_list_blank_line(self):
-        class opt(object):
-            filename = cli_mock.create_file('\na\nb\n\nc\n')
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c'],
-                                self.atest._file_list(options,
-                                                      opt_file='filename'))
-        os.unlink(options.filename)
-
-
-    def test_file_list_opt_list_one(self):
-        class opt(object):
-            hlist = 'a'
-        options = opt()
-        self.assertEqualNoOrder(['a'],
-                                self.atest._file_list(options,
-                                                      opt_list='hlist'))
-
-
-    def test_file_list_opt_list_space(self):
-        class opt(object):
-            hlist = 'a b c'
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c'],
-                                self.atest._file_list(options,
-                                                      opt_list='hlist'))
-
-
-    def test_file_list_opt_list_mix_space_comma(self):
-        class opt(object):
-            alist = 'a b,c,d e'
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e'],
-                                self.atest._file_list(options,
-                                                      opt_list='alist'))
-
-
-    def test_file_list_opt_list_mix_comma_space(self):
-        class opt(object):
-            alist = 'a b,c, d e'
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e'],
-                                self.atest._file_list(options,
-                                                      opt_list='alist'))
-
-
-    def test_file_list_opt_list_end_comma_space(self):
-        class opt(object):
-            alist = 'a b, ,c,, d e, '
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e'],
-                                self.atest._file_list(options,
-                                                      opt_list='alist'))
-
-
-    def test_file_list_add_on_space(self):
-        class opt(object):
-            pass
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c'],
-                                self.atest._file_list(options,
-                                                      add_on=['a','c','b']))
-
-
-    def test_file_list_add_on_mix_space_comma(self):
-        class opt(object):
-            pass
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c', 'd'],
-                                self.atest._file_list(options,
-                                                      add_on=['a', 'c',
-                                                              'b,d']))
-
-
-    def test_file_list_add_on_mix_comma_space(self):
-        class opt(object):
-            pass
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c', 'd'],
-                                self.atest._file_list(options,
-                                                      add_on=['a', 'c',
-                                                              'b,', 'd']))
-
-
-    def test_file_list_add_on_end_comma_space(self):
-        class opt(object):
-            pass
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c', 'd'],
-                                self.atest._file_list(options,
-                                                      add_on=['a', 'c', 'b,',
-                                                              'd,', ',']))
-
-
-    def test_file_list_all_opt(self):
-        class opt(object):
-            afile = cli_mock.create_file('f\ng\nh\n')
-            alist = 'a b,c,d e'
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e',
-                                 'f', 'g', 'h', 'i', 'j'],
-                                self.atest._file_list(options,
-                                                      opt_file='afile',
-                                                      opt_list='alist',
-                                                      add_on=['i', 'j']))
-
-
-    def test_file_list_all_opt_empty_file(self):
-        self.god.mock_io()
-        class opt(object):
-            hfile = cli_mock.create_file('')
-            hlist = 'a b,c,d e'
-        options = opt()
-        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
-        self.assertRaises(cli_mock.ExitException,
-                          self.atest._file_list,
-                          options,
-                          opt_file='hfile',
-                          opt_list='hlist',
-                          add_on=['i', 'j'])
-        (output, err) = self.god.unmock_io()
-        self.god.check_playback()
-        self.assert_(err.find(options.hfile) >= 0)
-
-
-    def test_file_list_all_opt_in_common(self):
-        class opt(object):
-            afile = cli_mock.create_file('f\nc\na\n')
-            alist = 'a b,c,d e'
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e',
-                                 'f', 'i', 'j'],
-                                self.atest._file_list(options,
-                                                      opt_file='afile',
-                                                      opt_list='alist',
-                                                      add_on=['i','j,d']))
-
-
-    def test_file_list_all_opt_in_common_space(self):
-        class opt(object):
-            afile = cli_mock.create_file('a b c\nd,e\nf\ng')
-            alist = 'a b,c,d h'
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e',
-                                 'f', 'g', 'h', 'i', 'j'],
-                                self.atest._file_list(options,
-                                                      opt_file='afile',
-                                                      opt_list='alist',
-                                                      add_on=['i','j,d']))
-
-
-    def test_file_list_all_opt_in_common_weird(self):
-        class opt(object):
-            afile = cli_mock.create_file('a b c\nd,e\nf\ng, \n, ,,')
-            alist = 'a b,c,d h, ,  ,,	'
-        options = opt()
-        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e',
-                                 'f', 'g', 'h', 'i', 'j'],
-                                self.atest._file_list(options,
-                                                      opt_file='afile',
-                                                      opt_list='alist',
-                                                      add_on=['i','j,d']))
+        super(atest_unittest, self).tearDown()
 
 
     def test_invalid_arg_kill(self):
@@ -411,16 +366,15 @@
                                'This is partly bad', 'item0', 'item1'])
 
 
-    def test_parse_with_flist_add_on(self):
+    def test_parse_add_on(self):
         flist = cli_mock.create_file('host1\nhost2\nleft2')
         sys.argv = ['atest', '--web', 'fooweb', '--parse',
                     '--kill-on-failure', 'left1', 'left2', '-M', flist]
         self.atest.parser.add_option('-M', '--mlist', type='string')
-        (options, leftover) = self.atest.parse_with_flist([('hosts',
-                                                            'mlist',
-                                                            [],
-                                                            True)],
-                                                          None)
+        item_info = topic_common.item_parse_info(attribute_name='hosts',
+                                                 filename_option='mlist',
+                                                 use_leftover=True)
+        (options, leftover) = self.atest.parse([item_info])
         self.assertEqualNoOrder(self.atest.hosts,
                                 ['left1', 'left2', 'host1', 'host2'])
 
@@ -434,16 +388,14 @@
         self.assertEqual(leftover, [])
 
 
-    def test_parse_with_flist_no_add_on(self):
+    def test_parse_no_add_on(self):
         flist = cli_mock.create_file('host1\nhost2\nleft2')
         sys.argv = ['atest', '--web', 'fooweb', '--parse', '-g',
                     '--kill-on-failure', 'left1', 'left2', '-M', flist]
         self.atest.parser.add_option('-M', '--mlist', type='string')
-        (options, leftover) = self.atest.parse_with_flist([('hosts',
-                                                            'mlist',
-                                                            [],
-                                                            False)],
-                                                          None)
+        item_info = topic_common.item_parse_info(attribute_name='hosts',
+                                                 filename_option='mlist')
+        (options, leftover) = self.atest.parse([item_info])
         self.assertEqualNoOrder(self.atest.hosts,
                                 ['left2', 'host1', 'host2'])
 
@@ -457,7 +409,7 @@
         self.assertEqual(leftover, ['left1', 'left2'])
 
 
-    def test_parse_with_flists_add_on_first(self):
+    def test_parse_add_on_first(self):
         flist = cli_mock.create_file('host1\nhost2\nleft2')
         ulist = cli_mock.create_file('user1\nuser2\nuser3\n')
         sys.argv = ['atest', '-g', '--parse', '--ulist', ulist,
@@ -466,15 +418,14 @@
         self.atest.parser.add_option('-M', '--mlist', type='string')
         self.atest.parser.add_option('-U', '--ulist', type='string')
         self.atest.parser.add_option('-u', '--user', type='string')
-        (options, leftover) = self.atest.parse_with_flist([('hosts',
-                                                            'mlist',
-                                                            '',
-                                                            True),
-                                                           ('users',
-                                                            'ulist',
-                                                            'user',
-                                                            False)],
-                                                          None)
+        host_info = topic_common.item_parse_info(attribute_name='hosts',
+                                                 filename_option='mlist',
+                                                 use_leftover=True)
+        user_info = topic_common.item_parse_info(attribute_name='users',
+                                                 inline_option='user',
+                                                 filename_option='ulist')
+
+        (options, leftover) = self.atest.parse([host_info, user_info])
         self.assertEqualNoOrder(self.atest.hosts,
                                 ['left1', 'left2', 'host1', 'host2'])
         self.assertEqualNoOrder(self.atest.users,
@@ -493,7 +444,7 @@
         self.assertEqual(leftover, [])
 
 
-    def test_parse_with_flists_add_on_second(self):
+    def test_parse_add_on_second(self):
         flist = cli_mock.create_file('host1\nhost2\nleft2')
         ulist = cli_mock.create_file('user1\nuser2\nuser3\n')
         sys.argv = ['atest', '-g', '--parse', '-U', ulist,
@@ -502,15 +453,14 @@
         self.atest.parser.add_option('-M', '--mlist', type='string')
         self.atest.parser.add_option('-U', '--ulist', type='string')
         self.atest.parser.add_option('-u', '--user', type='string')
-        (options, leftover) = self.atest.parse_with_flist([('users',
-                                                            'ulist',
-                                                            'user',
-                                                            False),
-                                                           ('hosts',
-                                                            'mlist',
-                                                            '',
-                                                            True)],
-                                                          None)
+        host_info = topic_common.item_parse_info(attribute_name='hosts',
+                                                 filename_option='mlist',
+                                                 use_leftover=True)
+        user_info = topic_common.item_parse_info(attribute_name='users',
+                                                 inline_option='user',
+                                                 filename_option='ulist')
+        (options, leftover) = self.atest.parse([host_info, user_info])
+
         self.assertEqualNoOrder(self.atest.hosts,
                                 ['left1', 'left2', 'host1', 'host2'])
         self.assertEqualNoOrder(self.atest.users,
@@ -529,7 +479,7 @@
         self.assertEqual(leftover, [])
 
 
-    def test_parse_with_flists_all_opts(self):
+    def test_parse_all_opts(self):
         flist = cli_mock.create_file('host1\nhost2\nleft2')
         ulist = cli_mock.create_file('user1\nuser2\nuser3\n')
         sys.argv = ['atest', '-g', '--parse', '--ulist', ulist,
@@ -538,15 +488,13 @@
         self.atest.parser.add_option('-M', '--mlist', type='string')
         self.atest.parser.add_option('-U', '--ulist', type='string')
         self.atest.parser.add_option('-u', '--user', type='string')
-        (options, leftover) = self.atest.parse_with_flist([('users',
-                                                            'ulist',
-                                                            'user',
-                                                            False),
-                                                           ('hosts',
-                                                            'mlist',
-                                                            '',
-                                                            True)],
-                                                          None)
+        host_info = topic_common.item_parse_info(attribute_name='hosts',
+                                                 filename_option='mlist',
+                                                 use_leftover=True)
+        user_info = topic_common.item_parse_info(attribute_name='users',
+                                                 inline_option='user',
+                                                 filename_option='ulist')
+        (options, leftover) = self.atest.parse([host_info, user_info])
         self.assertEqualNoOrder(self.atest.hosts,
                                 ['left1', 'left2', 'host1', 'host2'])
         self.assertEqualNoOrder(self.atest.users,
@@ -565,7 +513,7 @@
         self.assertEqual(leftover, [])
 
 
-    def test_parse_with_flists_no_add_on(self):
+    def test_parse_no_add_on(self):
         flist = cli_mock.create_file('host1\nhost2\nleft2')
         ulist = cli_mock.create_file('user1\nuser2\nuser3\n')
         sys.argv = ['atest', '-U', ulist,
@@ -573,15 +521,13 @@
         self.atest.parser.add_option('-M', '--mlist', type='string')
         self.atest.parser.add_option('-U', '--ulist', type='string')
         self.atest.parser.add_option('-u', '--user', type='string')
-        (options, leftover) = self.atest.parse_with_flist([('hosts',
-                                                            'mlist',
-                                                            '',
-                                                            False),
-                                                           ('users',
-                                                            'ulist',
-                                                            'user',
-                                                            False)],
-                                                          None)
+        host_info = topic_common.item_parse_info(attribute_name='hosts',
+                                                 filename_option='mlist',
+                                                 use_leftover=True)
+        user_info = topic_common.item_parse_info(attribute_name='users',
+                                                 inline_option='user',
+                                                 filename_option='ulist')
+        (options, leftover) = self.atest.parse([host_info, user_info])
         self.assertEqualNoOrder(self.atest.hosts,
                                 ['left2', 'host1', 'host2'])
         self.assertEqualNoOrder(self.atest.users,
@@ -599,21 +545,17 @@
         self.assertEqual(leftover, [])
 
 
-    def test_parse_with_flists_no_flist_add_on(self):
+    def test_parse_no_flist_add_on(self):
         sys.argv = ['atest', '-g', '--parse', '-u', 'myuser,youruser',
                     '--kill-on-failure', 'left1', 'left2']
         self.atest.parser.add_option('-M', '--mlist', type='string')
         self.atest.parser.add_option('-U', '--ulist', type='string')
         self.atest.parser.add_option('-u', '--user', type='string')
-        (options, leftover) = self.atest.parse_with_flist([('users',
-                                                            '',
-                                                            'user',
-                                                            False),
-                                                           ('hosts',
-                                                            '',
-                                                            '',
-                                                            True)],
-                                                          None)
+        host_info = topic_common.item_parse_info(attribute_name='hosts',
+                                                 use_leftover=True)
+        user_info = topic_common.item_parse_info(attribute_name='users',
+                                                 inline_option='user')
+        (options, leftover) = self.atest.parse([host_info, user_info])
         self.assertEqualNoOrder(self.atest.hosts,
                                 ['left1', 'left2'])
         self.assertEqualNoOrder(self.atest.users,
@@ -631,20 +573,16 @@
         self.assertEqual(leftover, [])
 
 
-    def test_parse_with_flists_no_flist_no_add_on(self):
+    def test_parse_no_flist_no_add_on(self):
         sys.argv = ['atest', '-u', 'myuser,youruser', '--kill-on-failure',
                     '-a', 'acl1,acl2']
         self.atest.parser.add_option('-u', '--user', type='string')
         self.atest.parser.add_option('-a', '--acl', type='string')
-        (options, leftover) = self.atest.parse_with_flist([('users',
-                                                            '',
-                                                            'user',
-                                                            False),
-                                                           ('acls',
-                                                            '',
-                                                            'acl',
-                                                            False)],
-                                                          None)
+        acl_info = topic_common.item_parse_info(attribute_name='acls',
+                                                inline_option='acl')
+        user_info = topic_common.item_parse_info(attribute_name='users',
+                                                 inline_option='user')
+        (options, leftover) = self.atest.parse([user_info, acl_info])
         self.assertEqualNoOrder(self.atest.acls,
                                 ['acl1', 'acl2'])
         self.assertEqualNoOrder(self.atest.users,
@@ -661,14 +599,13 @@
         self.assertEqual(leftover, [])
 
 
-    def test_parse_with_flists_req_items_ok(self):
+    def test_parse_req_items_ok(self):
         sys.argv = ['atest', '-u', 'myuser,youruser']
         self.atest.parser.add_option('-u', '--user', type='string')
-        (options, leftover) = self.atest.parse_with_flist([('users',
-                                                            '',
-                                                            'user',
-                                                            False)],
-                                                          'users')
+        user_info = topic_common.item_parse_info(attribute_name='users',
+                                                 inline_option='user')
+        (options, leftover) = self.atest.parse([user_info],
+                                               req_items='users')
         self.assertEqualNoOrder(self.atest.users,
                                 ['myuser', 'youruser'])
 
@@ -682,15 +619,18 @@
         self.assertEqual(leftover, [])
 
 
-    def test_parse_with_flists_req_items_missing(self):
+    def test_parse_req_items_missing(self):
         sys.argv = ['atest', '-u', 'myuser,youruser', '--kill-on-failure']
         self.atest.parser.add_option('-u', '--user', type='string')
+        acl_info = topic_common.item_parse_info(attribute_name='acls',
+                                                inline_option='acl')
+        user_info = topic_common.item_parse_info(attribute_name='users',
+                                                 inline_option='user')
         self.god.mock_io()
         sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
         self.assertRaises(cli_mock.ExitException,
-                          self.atest.parse_with_flist,
-                          [('users', '', 'user', False),
-                           ('acls', '', 'acl', False)],
+                          self.atest.parse,
+                          [user_info, acl_info],
                           'acls')
         self.assertEqualNoOrder(self.atest.users,
                                 ['myuser', 'youruser'])
diff --git a/cli/user.py b/cli/user.py
index 8257c79..0e4fd49 100755
--- a/cli/user.py
+++ b/cli/user.py
@@ -36,14 +36,10 @@
                                default=None,
                                metavar='USER_FLIST')
 
-
-    def parse(self, flists=None, req_items='users'):
-        """Consume the common user options"""
-        if flists:
-            flists.append(('users', 'ulist', '', True))
-        else:
-            flists = [('users', 'ulist', '', True)]
-        return self.parse_with_flist(flists, req_items)
+        self.topic_parse_info = topic_common.item_parse_info(
+            attribute_name='users',
+            filename_option='ulist',
+            use_leftover=True)
 
 
     def get_items(self):
@@ -70,7 +66,7 @@
 
 
     def parse(self):
-        (options, leftover) = super(user_list, self).parse(req_items=None)
+        (options, leftover) = super(user_list, self).parse()
         self.acl = options.acl
         self.access_level = options.access_level
         return (options, leftover)