Code

Updated to version 1.4 (python 2.2) version of pygettext
[roundup.git] / roundup-admin
index 8ea917329913e3c944b989b9f62ca58c1c6ebe61..6a1d5c078efa1716ccc4a5e9e17d209857a4a983 100755 (executable)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: roundup-admin,v 1.38 2001-11-05 23:45:40 richard Exp $
+# $Id: roundup-admin,v 1.55 2001-12-17 03:52:47 richard Exp $
 
-import sys
-if int(sys.version[0]) < 2:
-    print 'Roundup requires python 2.0 or later.'
-    sys.exit(1)
+# python version check
+from roundup import version_check
 
-import string, os, getpass, getopt, re
+import sys, os, getpass, getopt, re, UserDict
 try:
     import csv
 except ImportError:
@@ -31,10 +29,32 @@ except ImportError:
 from roundup import date, hyperdb, roundupdb, init, password
 import roundup.instance
 
+class CommandDict(UserDict.UserDict):
+    '''Simple dictionary that lets us do lookups using partial keys.
+
+    Original code submitted by Engelbert Gruber.
+    '''
+    _marker = []
+    def get(self, key, default=_marker):
+        if self.data.has_key(key):
+            return [(key, self.data[key])]
+        keylist = self.data.keys()
+        keylist.sort()
+        l = []
+        for ki in keylist:
+            if ki.startswith(key):
+                l.append((ki, self.data[ki]))
+        if not l and default is self._marker:
+            raise KeyError, key
+        return l
+
+class UsageError(ValueError):
+    pass
+
 class AdminTool:
 
     def __init__(self):
-        self.commands = {}
+        self.commands = CommandDict()
         for k in AdminTool.__dict__.keys():
             if k[:3] == 'do_':
                 self.commands[k[3:]] = getattr(self, k)
@@ -42,9 +62,11 @@ class AdminTool:
         for k in AdminTool.__dict__.keys():
             if k[:5] == 'help_':
                 self.help[k[5:]] = getattr(self, k)
+        self.instance_home = ''
+        self.db = None
 
-    def usage(message=''):
-        if message: message = 'Problem: '+message+'\n'
+    def usage(self, message=''):
+        if message: message = 'Problem: '+message+'\n\n'
         print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments>
 
 Help:
@@ -63,9 +85,35 @@ Options:
         commands = ['']
         for command in self.commands.values():
             h = command.__doc__.split('\n')[0]
-            commands.append(h[7:])
+            commands.append(' '+h[7:])
         commands.sort()
-        print '\n '.join(commands)
+        commands.append(
+'Commands may be abbreviated as long as the abbreviation matches only one')
+        commands.append('command, e.g. l == li == lis == list.')
+        print '\n'.join(commands)
+        print
+
+    def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')):
+       commands = self.commands.values()
+        def sortfun(a, b):
+            return cmp(a.__name__, b.__name__)
+        commands.sort(sortfun)
+       for command in commands:
+            h = command.__doc__.split('\n')
+            name = command.__name__[3:]
+            usage = h[0]
+            print '''
+<tr><td valign=top><strong>%(name)s</strong></td>
+    <td><tt>%(usage)s</tt><p>
+<pre>'''%locals()
+            indent = indent_re.match(h[3])
+            if indent: indent = len(indent.group(1))
+            for line in h[3:]:
+                if indent:
+                    print line[indent:]
+                else:
+                    print line
+            print '</pre></td></tr>\n'
 
     def help_all(self):
         print '''
@@ -131,13 +179,22 @@ Command help:
         initopts  -- init command options
         all       -- all available help
         '''
-        help = self.help.get(args[0], None)
-        if help:
-            help()
-            return
-        help = self.commands.get(args[0], None)
-        if help:
-            # display the help, removing the docsring indent
+        topic = args[0]
+
+        # try help_ methods
+        if self.help.has_key(topic):
+            self.help[topic]()
+            return 0
+
+        # try command docstrings
+        try:
+            l = self.commands.get(topic)
+        except KeyError:
+            print 'Sorry, no help for "%s"'%topic
+            return 1
+
+        # display the help for each match, removing the docsring indent
+        for name, help in l:
             lines = nl_re.split(help.__doc__)
             print lines[0]
             indent = indent_re.match(lines[1])
@@ -147,8 +204,7 @@ Command help:
                     print line[indent:]
                 else:
                     print line
-        else:
-            print 'Sorry, no help for "%s"'%args[0]
+        return 0
 
     def help_initopts(self):
         import roundup.templates
@@ -159,8 +215,8 @@ Command help:
         print 'Back ends:', ', '.join(backends)
 
 
-    def do_init(self, instance_home, args):
-        '''Usage: init [template [backend [admin password]]]
+    def do_initialise(self, instance_home, args):
+        '''Usage: initialise [template [backend [admin password]]]
         Initialise a new Roundup instance.
 
         The command will prompt for the instance home directory (if not supplied
@@ -170,6 +226,8 @@ Command help:
 
         See also initopts help.
         '''
+        if len(args) < 1:
+            raise UsageError, 'Not enough arguments supplied'
         # select template
         import roundup.templates
         templates = roundup.templates.listTemplates()
@@ -208,19 +266,33 @@ Command help:
 
         Retrieves the property value of the nodes specified by the designators.
         '''
+        if len(args) < 2:
+            raise UsageError, 'Not enough arguments supplied'
         propname = args[0]
-        designators = string.split(args[1], ',')
+        designators = args[1].split(',')
         l = []
         for designator in designators:
+            # decode the node designator
             try:
                 classname, nodeid = roundupdb.splitDesignator(designator)
             except roundupdb.DesignatorError, message:
-                print 'Error: %s'%message
-                return 1
-            if self.comma_sep:
-                l.append(self.db.getclass(classname).get(nodeid, propname))
-            else:
-                print self.db.getclass(classname).get(nodeid, propname)
+                raise UsageError, message
+
+            # get the class
+            try:
+                cl = self.db.getclass(classname)
+            except KeyError:
+                raise UsageError, 'invalid class "%s"'%classname
+            try:
+                if self.comma_sep:
+                    l.append(cl.get(nodeid, propname))
+                else:
+                    print cl.get(nodeid, propname)
+            except IndexError:
+                raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
+            except KeyError:
+                raise UsageError, 'no such %s property "%s"'%(classname,
+                    propname)
         if self.comma_sep:
             print ','.join(l)
         return 0
@@ -232,36 +304,60 @@ Command help:
 
         Sets the property to the value for all designators given.
         '''
+        if len(args) < 2:
+            raise UsageError, 'Not enough arguments supplied'
         from roundup import hyperdb
 
-        designators = string.split(args[0], ',')
+        designators = args[0].split(',')
         props = {}
         for prop in args[1:]:
-            key, value = prop.split('=')
+            if prop.find('=') == -1:
+                raise UsageError, 'argument "%s" not propname=value'%prop
+            try:
+                key, value = prop.split('=')
+            except ValueError:
+                raise UsageError, 'argument "%s" not propname=value'%prop
             props[key] = value
         for designator in designators:
+            # decode the node designator
             try:
                 classname, nodeid = roundupdb.splitDesignator(designator)
             except roundupdb.DesignatorError, message:
-                print 'Error: %s'%message
-                return 1
-            cl = self.db.getclass(classname)
+                raise UsageError, message
+
+            # get the class
+            try:
+                cl = self.db.getclass(classname)
+            except KeyError:
+                raise UsageError, 'invalid class "%s"'%classname
+
             properties = cl.getprops()
             for key, value in props.items():
-                type =  properties[key]
-                if isinstance(type, hyperdb.String):
+                proptype =  properties[key]
+                if isinstance(proptype, hyperdb.String):
                     continue
-                elif isinstance(type, hyperdb.Password):
+                elif isinstance(proptype, hyperdb.Password):
                     props[key] = password.Password(value)
-                elif isinstance(type, hyperdb.Date):
-                    props[key] = date.Date(value)
-                elif isinstance(type, hyperdb.Interval):
-                    props[key] = date.Interval(value)
-                elif isinstance(type, hyperdb.Link):
+                elif isinstance(proptype, hyperdb.Date):
+                    try:
+                        props[key] = date.Date(value)
+                    except ValueError, message:
+                        raise UsageError, '"%s": %s'%(value, message)
+                elif isinstance(proptype, hyperdb.Interval):
+                    try:
+                        props[key] = date.Interval(value)
+                    except ValueError, message:
+                        raise UsageError, '"%s": %s'%(value, message)
+                elif isinstance(proptype, hyperdb.Link):
                     props[key] = value
-                elif isinstance(type, hyperdb.Multilink):
+                elif isinstance(proptype, hyperdb.Multilink):
                     props[key] = value.split(',')
-            apply(cl.set, (nodeid, ), props)
+
+            # try the set
+            try:
+                apply(cl.set, (nodeid, ), props)
+            except (TypeError, IndexError, ValueError), message:
+                raise UsageError, message
         return 0
 
     def do_find(self, args):
@@ -271,36 +367,79 @@ Command help:
         Find the nodes of the given class with a given link property value. The
         value may be either the nodeid of the linked node, or its key value.
         '''
+        if len(args) < 1:
+            raise UsageError, 'Not enough arguments supplied'
         classname = args[0]
-        cl = self.db.getclass(classname)
+        # get the class
+        try:
+            cl = self.db.getclass(classname)
+        except KeyError:
+            raise UsageError, 'invalid class "%s"'%classname
+
+        # TODO: handle > 1 argument
+        # handle the propname=value argument
+        if args[1].find('=') == -1:
+            raise UsageError, 'argument "%s" not propname=value'%prop
+        try:
+            propname, value = args[1].split('=')
+        except ValueError:
+            raise UsageError, 'argument "%s" not propname=value'%prop
 
-        # look up the linked-to class and get the nodeid that has the value
-        propname, value = args[1].split('=')
+        # if the value isn't a number, look up the linked class to get the
+        # number
         num_re = re.compile('^\d+$')
         if not num_re.match(value):
-            propcl = cl.properties[propname]
-            if (not isinstance(propcl, hyperdb.Link) and not
-                    isinstance(type, hyperdb.Multilink)):
-                print 'You may only "find" link properties'
-                return 1
-            propcl = self.db.getclass(propcl.classname)
-            value = propcl.lookup(value)
-
-        # now do the find
-        if self.comma_sep:
-            print ','.join(cl.find(**{propname: value}))
-        else:
-            print cl.find(**{propname: value})
+            # get the property
+            try:
+                property = cl.properties[propname]
+            except KeyError:
+                raise UsageError, '%s has no property "%s"'%(classname,
+                    propname)
+
+            # make sure it's a link
+            if (not isinstance(property, hyperdb.Link) and not
+                    isinstance(property, hyperdb.Multilink)):
+                raise UsageError, 'You may only "find" link properties'
+
+            # get the linked-to class and look up the key property
+            link_class = self.db.getclass(property.classname)
+            try:
+                value = link_class.lookup(value)
+            except TypeError:
+                raise UsageError, '%s has no key property"'%link_class.classname
+            except KeyError:
+                raise UsageError, '%s has no entry "%s"'%(link_class.classname,
+                    propname)
+
+        # now do the find 
+        try:
+            if self.comma_sep:
+                print ','.join(apply(cl.find, (), {propname: value}))
+            else:
+                print apply(cl.find, (), {propname: value})
+        except KeyError:
+            raise UsageError, '%s has no property "%s"'%(classname,
+                propname)
+        except (ValueError, TypeError), message:
+            raise UsageError, message
         return 0
 
-    def do_spec(self, args):
-        '''Usage: spec classname
+    def do_specification(self, args):
+        '''Usage: specification classname
         Show the properties for a classname.
 
         This lists the properties for a given class.
         '''
+        if len(args) < 1:
+            raise UsageError, 'Not enough arguments supplied'
         classname = args[0]
-        cl = self.db.getclass(classname)
+        # get the class
+        try:
+            cl = self.db.getclass(classname)
+        except KeyError:
+            raise UsageError, 'invalid class "%s"'%classname
+
+        # get the key property
         keyprop = cl.getkey()
         for key, value in cl.properties.items():
             if keyprop == key:
@@ -308,6 +447,33 @@ Command help:
             else:
                 print '%s: %s'%(key, value)
 
+    def do_display(self, args):
+        '''Usage: display designator
+        Show the property values for the given node.
+
+        This lists the properties and their associated values for the given
+        node.
+        '''
+        if len(args) < 1:
+            raise UsageError, 'Not enough arguments supplied'
+
+        # decode the node designator
+        try:
+            classname, nodeid = roundupdb.splitDesignator(args[0])
+        except roundupdb.DesignatorError, message:
+            raise UsageError, message
+
+        # get the class
+        try:
+            cl = self.db.getclass(classname)
+        except KeyError:
+            raise UsageError, 'invalid class "%s"'%classname
+
+        # display the values
+        for key in cl.properties.keys():
+            value = cl.get(nodeid, key)
+            print '%s: %s'%(key, value)
+
     def do_create(self, args):
         '''Usage: create classname property=value ...
         Create a new entry of a given class.
@@ -316,10 +482,19 @@ Command help:
         name=value arguments provided on the command line after the "create"
         command.
         '''
+        if len(args) < 1:
+            raise UsageError, 'Not enough arguments supplied'
         from roundup import hyperdb
 
         classname = args[0]
-        cl = self.db.getclass(classname)
+
+        # get the class
+        try:
+            cl = self.db.getclass(classname)
+        except KeyError:
+            raise UsageError, 'invalid class "%s"'%classname
+
+        # now do a create
         props = {}
         properties = cl.getprops(protected = 0)
         if len(args) == 1:
@@ -342,26 +517,46 @@ Command help:
         else:
             # use the args
             for prop in args[1:]:
-                key, value = prop.split('=')
+                if prop.find('=') == -1:
+                    raise UsageError, 'argument "%s" not propname=value'%prop
+                try:
+                    key, value = prop.split('=')
+                except ValueError:
+                    raise UsageError, 'argument "%s" not propname=value'%prop
                 props[key] = value 
 
         # convert types
         for key in props.keys():
-            type =  properties[key]
-            if isinstance(type, hyperdb.Date):
-                props[key] = date.Date(value)
-            elif isinstance(type, hyperdb.Interval):
-                props[key] = date.Interval(value)
-            elif isinstance(type, hyperdb.Password):
+            # get the property
+            try:
+                proptype = properties[key]
+            except KeyError:
+                raise UsageError, '%s has no property "%s"'%(classname, key)
+
+            if isinstance(proptype, hyperdb.Date):
+                try:
+                    props[key] = date.Date(value)
+                except ValueError, message:
+                    raise UsageError, '"%s": %s'%(value, message)
+            elif isinstance(proptype, hyperdb.Interval):
+                try:
+                    props[key] = date.Interval(value)
+                except ValueError, message:
+                    raise UsageError, '"%s": %s'%(value, message)
+            elif isinstance(proptype, hyperdb.Password):
                 props[key] = password.Password(value)
-            elif isinstance(type, hyperdb.Multilink):
+            elif isinstance(proptype, hyperdb.Multilink):
                 props[key] = value.split(',')
 
+        # check for the key property
         if cl.getkey() and not props.has_key(cl.getkey()):
-            print "You must provide the '%s' property."%cl.getkey()
-        else:
-            print apply(cl.create, (), props)
+            raise UsageError, "you must provide the '%s' property."%cl.getkey()
 
+        # do the actual create
+        try:
+            print apply(cl.create, (), props)
+        except (TypeError, IndexError, ValueError), message:
+            raise UsageError, message
         return 0
 
     def do_list(self, args):
@@ -373,17 +568,30 @@ Command help:
         in order: the key, "name", "title" and then the first property,
         alphabetically.
         '''
+        if len(args) < 1:
+            raise UsageError, 'Not enough arguments supplied'
         classname = args[0]
-        cl = self.db.getclass(classname)
+
+        # get the class
+        try:
+            cl = self.db.getclass(classname)
+        except KeyError:
+            raise UsageError, 'invalid class "%s"'%classname
+
+        # figure the property
         if len(args) > 1:
             key = args[1]
         else:
             key = cl.labelprop()
+
         if self.comma_sep:
             print ','.join(cl.list())
         else:
             for nodeid in cl.list():
-                value = cl.get(nodeid, key)
+                try:
+                    value = cl.get(nodeid, key)
+                except KeyError:
+                    raise UsageError, '%s has no property "%s"'%(classname, key)
                 print "%4s: %s"%(nodeid, value)
         return 0
 
@@ -402,26 +610,54 @@ Command help:
           3  usability 
           4  feature   
         '''
+        if len(args) < 1:
+            raise UsageError, 'Not enough arguments supplied'
         classname = args[0]
-        cl = self.db.getclass(classname)
+
+        # get the class
+        try:
+            cl = self.db.getclass(classname)
+        except KeyError:
+            raise UsageError, 'invalid class "%s"'%classname
+
+        # figure the property names to display
         if len(args) > 1:
             prop_names = args[1].split(',')
+            all_props = cl.getprops()
+            for prop_name in prop_names:
+                if not all_props.has_key(prop_name):
+                    raise UsageError, '%s has no property "%s"'%(classname,
+                        prop_name)
         else:
             prop_names = cl.getprops().keys()
+
+        # now figure column widths
         props = []
-        for name in prop_names:
-            if ':' in name:
-                name, width = name.split(':')
-                props.append((name, int(width)))
+        for spec in prop_names:
+            if ':' in spec:
+                try:
+                    name, width = spec.split(':')
+                except (ValueError, TypeError):
+                    raise UsageError, '"%s" not name:width'%spec
+                props.append((spec, int(width)))
             else:
-                props.append((name, len(name)))
+                props.append((spec, len(spec)))
+
+        # now display the heading
+        print ' '.join([name.capitalize() for name, width in props])
 
-        print ' '.join([string.capitalize(name) for name, width in props])
+        # and the table data
         for nodeid in cl.list():
             l = []
             for name, width in props:
                 if name != 'id':
-                    value = str(cl.get(nodeid, name))
+                    try:
+                        value = str(cl.get(nodeid, name))
+                    except KeyError:
+                        # we already checked if the property is valid - a
+                        # KeyError here means the node just doesn't have a
+                        # value for it
+                        value = ''
                 else:
                     value = str(nodeid)
                 f = '%%-%ds'%width
@@ -435,13 +671,46 @@ Command help:
 
         Lists the journal entries for the node identified by the designator.
         '''
+        if len(args) < 1:
+            raise UsageError, 'Not enough arguments supplied'
         try:
             classname, nodeid = roundupdb.splitDesignator(args[0])
         except roundupdb.DesignatorError, message:
-            print 'Error: %s'%message
-            return 1
+            raise UsageError, message
+
         # TODO: handle the -c option?
-        print self.db.getclass(classname).history(nodeid)
+        try:
+            print self.db.getclass(classname).history(nodeid)
+        except KeyError:
+            raise UsageError, 'no such class "%s"'%classname
+        except IndexError:
+            raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
+        return 0
+
+    def do_commit(self, args):
+        '''Usage: commit
+        Commit all changes made to the database.
+
+        The changes made during an interactive session are not
+        automatically written to the database - they must be committed
+        using this command.
+
+        One-off commands on the command-line are automatically committed if
+        they are successful.
+        '''
+        self.db.commit()
+        return 0
+
+    def do_rollback(self, args):
+        '''Usage: rollback
+        Undo all changes that are pending commit to the database.
+
+        The changes made during an interactive session are not
+        automatically written to the database - they must be committed
+        manually. This command undoes all those changes, so a commit
+        immediately after would make no changes to the database.
+        '''
+        self.db.rollback()
         return 0
 
     def do_retire(self, args):
@@ -451,14 +720,20 @@ Command help:
         This action indicates that a particular node is not to be retrieved by
         the list or find commands, and its key value may be re-used.
         '''
-        designators = string.split(args[0], ',')
+        if len(args) < 1:
+            raise UsageError, 'Not enough arguments supplied'
+        designators = args[0].split(',')
         for designator in designators:
             try:
                 classname, nodeid = roundupdb.splitDesignator(designator)
             except roundupdb.DesignatorError, message:
-                print 'Error: %s'%message
-                return 1
-            self.db.getclass(classname).retire(nodeid)
+                raise UsageError, message
+            try:
+                self.db.getclass(classname).retire(nodeid)
+            except KeyError:
+                raise UsageError, 'no such class "%s"'%classname
+            except IndexError:
+                raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
         return 0
 
     def do_export(self, args):
@@ -470,9 +745,8 @@ Command help:
         directory. The journals are not exported.
         '''
         if len(args) < 2:
-            print do_export.__doc__
-            return 1
-        classes = string.split(args[0], ',')
+            raise UsageError, 'Not enough arguments supplied'
+        classes = args[0].split(',')
         dir = args[1]
 
         # use the csv parser if we can - it's faster
@@ -481,22 +755,25 @@ Command help:
 
         # do all the classes specified
         for classname in classes:
-            cl = self.db.getclass(classname)
+            try:
+                cl = self.db.getclass(classname)
+            except KeyError:
+                raise UsageError, 'no such class "%s"'%classname
             f = open(os.path.join(dir, classname+'.csv'), 'w')
-            f.write(string.join(cl.properties.keys(), ':') + '\n')
+            f.write(':'.join(cl.properties.keys()) + '\n')
 
             # all nodes for this class
             properties = cl.properties.items()
             for nodeid in cl.list():
                 l = []
-                for prop, type in properties:
+                for prop, proptype in properties:
                     value = cl.get(nodeid, prop)
                     # convert data where needed
-                    if isinstance(type, hyperdb.Date):
+                    if isinstance(proptype, hyperdb.Date):
                         value = value.get_tuple()
-                    elif isinstance(type, hyperdb.Interval):
+                    elif isinstance(proptype, hyperdb.Interval):
                         value = value.get_tuple()
-                    elif isinstance(type, hyperdb.Password):
+                    elif isinstance(proptype, hyperdb.Password):
                         value = str(value)
                     l.append(repr(value))
 
@@ -526,17 +803,20 @@ Command help:
         the old data.)
         '''
         if len(args) < 2:
-            print do_import.__doc__
-            return 1
+            raise UsageError, 'Not enough arguments supplied'
         if csv is None:
-            print 'Sorry, you need the csv module to use this function.'
-            print 'Get it from: http://www.object-craft.com.au/projects/csv/'
-            return 1
+            raise UsageError, \
+                'Sorry, you need the csv module to use this function.\n'\
+                'Get it from: http://www.object-craft.com.au/projects/csv/'
 
         from roundup import hyperdb
 
         # ensure that the properties and the CSV file headings match
-        cl = self.db.getclass(args[0])
+        classname = args[0]
+        try:
+            cl = self.db.getclass(classname)
+        except KeyError:
+            raise UsageError, 'no such class "%s"'%classname
         f = open(args[1])
         p = csv.parser(field_sep=':')
         file_props = p.parse(f.readline())
@@ -545,8 +825,8 @@ Command help:
         m.sort()
         props.sort()
         if m != props:
-            print 'Import file doesn\'t define the same properties as "%s".'%args[0]
-            return 1
+            raise UsageError, 'Import file doesn\'t define the same '\
+                'properties as "%s".'%args[0]
 
         # loop through the file and create a node for each entry
         n = range(len(props))
@@ -566,13 +846,13 @@ Command help:
                 value = eval(l[i])
                 # Figure the property for this column
                 key = file_props[i]
-                type = cl.properties[key]
+                proptype = cl.properties[key]
                 # Convert for property type
-                if isinstance(type, hyperdb.Date):
+                if isinstance(proptype, hyperdb.Date):
                     value = date.Date(value)
-                elif isinstance(type, hyperdb.Interval):
+                elif isinstance(proptype, hyperdb.Interval):
                     value = date.Interval(value)
-                elif isinstance(type, hyperdb.Password):
+                elif isinstance(proptype, hyperdb.Password):
                     pwd = password.Password()
                     pwd.unpack(value)
                     value = pwd
@@ -601,36 +881,54 @@ Command help:
             self.help_all()
             return 0
 
+        # figure what the command is
+        try:
+            functions = self.commands.get(command)
+        except KeyError:
+            # not a valid command
+            print 'Unknown command "%s" ("help commands" for a list)'%command
+            return 1
+
+        # check for multiple matches
+        if len(functions) > 1:
+            print 'Multiple commands match "%s": %s'%(command,
+                ', '.join([i[0] for i in functions]))
+            return 1
+        command, function = functions[0]
+
         # make sure we have an instance_home
         while not self.instance_home:
             self.instance_home = raw_input('Enter instance home: ').strip()
 
         # before we open the db, we may be doing an init
-        if command == 'init':
-            return self.do_init(self.instance_home, args)
-
-        function = self.commands.get(command, None)
-
-        # not a valid command
-        if function is None:
-            print 'Unknown command "%s" ("help commands" for a list)'%command
-            return 1
+        if command == 'initialise':
+            return self.do_initialise(self.instance_home, args)
 
         # get the instance
-        instance = roundup.instance.open(self.instance_home)
-        self.db = instance.open('admin')
-
-        if len(args) < 2:
-            print function.__doc__
+        try:
+            instance = roundup.instance.open(self.instance_home)
+        except ValueError, message:
+            self.instance_home = ''
+            print "Couldn't open instance: %s"%message
             return 1
 
+        # only open the database once!
+        if not self.db:
+            self.db = instance.open('admin')
+
         # do the command
+        ret = 0
         try:
-            return function(args[1:])
-        finally:
-            self.db.close()
-
-        return 1
+            ret = function(args[1:])
+        except UsageError, message:
+            print 'Error: %s'%message
+            print function.__doc__
+            ret = 1
+        except:
+            import traceback
+            traceback.print_exc()
+            ret = 1
+        return ret
 
     def interactive(self, ws_re=re.compile(r'\s+')):
         '''Run in an interactive mode
@@ -646,15 +944,27 @@ Command help:
             try:
                 command = raw_input('roundup> ')
             except EOFError:
-                print '.. exit'
-                return 0
+                print 'exit...'
+                break
+            if not command: continue
             args = ws_re.split(command)
             if not args: continue
-            if args[0] in ('quit', 'exit'): return 0
+            if args[0] in ('quit', 'exit'): break
             self.run_command(args)
 
+        # exit.. check for transactions
+        if self.db and self.db.transactions:
+            commit = raw_input("There are unsaved changes. Commit them (y/N)? ")
+            if commit[0].lower() == 'y':
+                self.db.commit()
+        return 0
+
     def main(self):
-        opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
+        try:
+            opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
+        except getopt.GetoptError, e:
+            self.usage(str(e))
+            return 1
 
         # handle command-line args
         self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
@@ -667,7 +977,7 @@ Command help:
         self.comma_sep = 0
         for opt, arg in opts:
             if opt == '-h':
-                usage()
+                self.usage()
                 return 0
             if opt == '-i':
                 self.instance_home = arg
@@ -675,10 +985,13 @@ Command help:
                 self.comma_sep = 1
 
         # if no command - go interactive
+        ret = 0
         if not args:
-            return self.interactive()
-
-        self.run_command(args)
+            self.interactive()
+        else:
+            ret = self.run_command(args)
+            if self.db: self.db.commit()
+        return ret
 
 
 if __name__ == '__main__':
@@ -687,6 +1000,97 @@ if __name__ == '__main__':
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.54  2001/12/15 23:09:23  richard
+# Some cleanups in roundup-admin, also made it work again...
+#
+# Revision 1.53  2001/12/13 00:20:00  richard
+#  . Centralised the python version check code, bumped version to 2.1.1 (really
+#    needs to be 2.1.2, but that isn't released yet :)
+#
+# Revision 1.52  2001/12/12 21:47:45  richard
+#  . Message author's name appears in From: instead of roundup instance name
+#    (which still appears in the Reply-To:)
+#  . envelope-from is now set to the roundup-admin and not roundup itself so
+#    delivery reports aren't sent to roundup (thanks Patrick Ohly)
+#
+# Revision 1.51  2001/12/10 00:57:38  richard
+# From CHANGES:
+#  . Added the "display" command to the admin tool - displays a node's values
+#  . #489760 ] [issue] only subject
+#  . fixed the doc/index.html to include the quoting in the mail alias.
+#
+# Also:
+#  . fixed roundup-admin so it works with transactions
+#  . disabled the back_anydbm module if anydbm tries to use dumbdbm
+#
+# Revision 1.50  2001/12/02 05:06:16  richard
+# . We now use weakrefs in the Classes to keep the database reference, so
+#   the close() method on the database is no longer needed.
+#   I bumped the minimum python requirement up to 2.1 accordingly.
+# . #487480 ] roundup-server
+# . #487476 ] INSTALL.txt
+#
+# I also cleaned up the change message / post-edit stuff in the cgi client.
+# There's now a clearly marked "TODO: append the change note" where I believe
+# the change note should be added there. The "changes" list will obviously
+# have to be modified to be a dict of the changes, or somesuch.
+#
+# More testing needed.
+#
+# Revision 1.49  2001/12/01 07:17:50  richard
+# . We now have basic transaction support! Information is only written to
+#   the database when the commit() method is called. Only the anydbm
+#   backend is modified in this way - neither of the bsddb backends have been.
+#   The mail, admin and cgi interfaces all use commit (except the admin tool
+#   doesn't have a commit command, so interactive users can't commit...)
+# . Fixed login/registration forwarding the user to the right page (or not,
+#   on a failure)
+#
+# Revision 1.48  2001/11/27 22:32:03  richard
+# typo
+#
+# Revision 1.47  2001/11/26 22:55:56  richard
+# Feature:
+#  . Added INSTANCE_NAME to configuration - used in web and email to identify
+#    the instance.
+#  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
+#    signature info in e-mails.
+#  . Some more flexibility in the mail gateway and more error handling.
+#  . Login now takes you to the page you back to the were denied access to.
+#
+# Fixed:
+#  . Lots of bugs, thanks Roché and others on the devel mailing list!
+#
+# Revision 1.46  2001/11/21 03:40:54  richard
+# more new property handling
+#
+# Revision 1.45  2001/11/12 22:51:59  jhermann
+# Fixed option & associated error handling
+#
+# Revision 1.44  2001/11/12 22:01:06  richard
+# Fixed issues with nosy reaction and author copies.
+#
+# Revision 1.43  2001/11/09 22:33:28  richard
+# More error handling fixes.
+#
+# Revision 1.42  2001/11/09 10:11:08  richard
+#  . roundup-admin now handles all hyperdb exceptions
+#
+# Revision 1.41  2001/11/09 01:25:40  richard
+# Should parse with python 1.5.2 now.
+#
+# Revision 1.40  2001/11/08 04:42:00  richard
+# Expanded the already-abbreviated "initialise" and "specification" commands,
+# and added a comment to the command help about the abbreviation.
+#
+# Revision 1.39  2001/11/08 04:29:59  richard
+# roundup-admin now accepts abbreviated commands (eg. l = li = lis = list)
+# [thanks Engelbert Gruber for the inspiration]
+#
+# Revision 1.38  2001/11/05 23:45:40  richard
+# Fixed newuser_action so it sets the cookie with the unencrypted password.
+# Also made it present nicer error messages (not tracebacks).
+#
 # Revision 1.37  2001/10/23 01:00:18  richard
 # Re-enabled login and registration access after lopping them off via
 # disabling access for anonymous users.