Make BE working with non-ASCII username.
[be.git] / libbe / ui / command_line.py
index dd10954ef36787c1baf8f0addbc5b515d2be392c..614366d8a6edbea4320302be953ced1a66c48d7d 100644 (file)
@@ -1,22 +1,20 @@
-# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
-#                         Chris Ball <cjb@laptop.org>
-#                         Gianluca Montecchi <gian@grys.it>
-#                         Oleg Romanyshyn <oromanyshyn@panoramicfeedback.com>
-#                         W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2012 Chris Ball <cjb@laptop.org>
+#                         W. Trevor King <wking@tremily.us>
 #
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
+# This file is part of Bugs Everywhere.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
+# Bugs Everywhere is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 2 of the License, or (at your option) any
+# later version.
 #
-# You should have received a copy of the GNU General Public License along
-# with this program; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+# Bugs Everywhere is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# Bugs Everywhere.  If not, see <http://www.gnu.org/licenses/>.
 
 """
 A command line interface to Bugs Everywhere.
@@ -25,13 +23,19 @@ A command line interface to Bugs Everywhere.
 import optparse
 import os
 import sys
+import locale
 
 import libbe
 import libbe.bugdir
 import libbe.command
+import libbe.command.help
 import libbe.command.util
+import libbe.storage
 import libbe.version
 import libbe.ui.util.pager
+import libbe.util.encoding
+import libbe.util.http
+
 
 if libbe.TESTING == True:
     import doctest
@@ -89,11 +93,11 @@ class CmdOptionParser(optparse.OptionParser):
             if '_' in name: # reconstruct original option name
                 options[name.replace('_', '-')] = options.pop(name)
         for name,value in options.items():
+            argument = None
+            option = self._option_by_name[name]
+            if option.arg != None:
+                argument = option.arg
             if value == '--complete':
-                argument = None
-                option = self._option_by_name[name]
-                if option.arg != None:
-                    argument = option.arg
                 fragment = None
                 indices = [i for i,arg in enumerate(args)
                            if arg == '--complete']
@@ -111,29 +115,37 @@ class CmdOptionParser(optparse.OptionParser):
                 if i+1 < len(args):
                     fragment = args[i+1]
                 self.complete(argument, fragment)
+            elif argument is not None:
+                value = self.process_raw_argument(argument=argument, value=value)
+                options[name] = value
         for i,arg in enumerate(parsed_args):
+            if i > 0 and self.command.name == 'be':
+                break # let this pass through for the command parser to handle
+            elif i < len(self.command.args):
+                argument = self.command.args[i]
+            elif len(self.command.args) == 0:
+                break # command doesn't take arguments
+            else:
+                argument = self.command.args[-1]
+                if argument.repeatable == False:
+                    raise libbe.command.UserError('Too many arguments')
             if arg == '--complete':
-                if i > 0 and self.command.name == 'be':
-                    break # let this pass through for the command parser to handle
-                elif i < len(self.command.args):
-                    argument = self.command.args[i]
-                elif len(self.command.args) == 0:
-                    break # command doesn't take arguments
-                else:
-                    argument = self.command.args[-1]
-                    if argument.repeatable == False:
-                        raise libbe.command.UserError('Too many arguments')
                 fragment = None
                 if i < len(parsed_args) - 1:
                     fragment = parsed_args[i+1]
                 self.complete(argument, fragment)
-        if len(parsed_args) > len(self.command.args) \
-                and self.command.args[-1].repeatable == False:
+            else:
+                value = self.process_raw_argument(argument=argument, value=arg)
+                parsed_args[i] = value
+        if (len(parsed_args) > len(self.command.args) and
+            (len(self.command.args) == 0 or
+             self.command.args[-1].repeatable == False)):
             raise libbe.command.UserError('Too many arguments')
         for arg in self.command.args[len(parsed_args):]:
             if arg.optional == False:
-                raise libbe.command.UserError(
-                    'Missing required argument %s' % arg.metavar)
+                raise libbe.command.UsageError(
+                    command=self.command,
+                    message='Missing required argument %s' % arg.metavar)
         return (options, parsed_args)
 
     def callback(self, option, opt, value, parser):
@@ -157,6 +169,16 @@ class CmdOptionParser(optparse.OptionParser):
             print >> self.command.stdout, '\n'.join(comps)
         raise CallbackExit
 
+    def process_raw_argument(self, argument, value):
+        if value == argument.default:
+            return value
+        if argument.type == 'string':
+            if not hasattr(self, 'argv_encoding'):
+                self.argv_encoding = libbe.util.encoding.get_argv_encoding()
+            return unicode(value, self.argv_encoding)
+        return value
+
+
 class BE (libbe.command.Command):
     """Class for parsing the command line arguments for `be`.
     This class does not contain a useful _run() method.  Call this
@@ -186,7 +208,6 @@ class BE (libbe.command.Command):
     ... except CallbackExit:
     ...     print '  got callback'
     --help
-    --complete
     --version
     ...
     subscribe
@@ -211,6 +232,12 @@ class BE (libbe.command.Command):
                     arg=libbe.command.Argument(
                         name='repo', metavar='REPO', default='.',
                         completion_callback=libbe.command.util.complete_path)),
+                libbe.command.Option(name='server', short_name='s',
+                    help='Select BE command server (see `be help '
+                         'server`) rather than executing commands '
+                         'locally',
+                    arg=libbe.command.Argument(
+                        name='server', metavar='URL')),
                 libbe.command.Option(name='paginate',
                     help='Pipe all output into less (or if set, $PAGER).'),
                 libbe.command.Option(name='no-pager',
@@ -233,15 +260,26 @@ class BE (libbe.command.Command):
             Class = libbe.command.get_command_class(command_name=name)
             assert hasattr(Class, '__doc__') and Class.__doc__ != None, \
                 'Command class %s missing docstring' % Class
-            cmdlist.append((name, Class.__doc__.splitlines()[0]))
+            cmdlist.append((Class.name, Class.__doc__.splitlines()[0]))
         cmdlist.sort()
         longest_cmd_len = max([len(name) for name,desc in cmdlist])
         ret = ['Bugs Everywhere - Distributed bug tracking',
-               '', 'Supported commands']
+               '', 'Commands:']
         for name, desc in cmdlist:
             numExtraSpaces = longest_cmd_len-len(name)
-            ret.append('be %s%*s    %s' % (name, numExtraSpaces, '', desc))
-        ret.extend(['', 'Run', '  be help [command]', 'for more information.'])
+            ret.append('be {}{}    {}'.format(name, ' '*numExtraSpaces, desc))
+
+        ret.extend(['', 'Topics:'])
+        topic_list = [
+            (name,desc.splitlines()[0])
+            for name,desc in sorted(libbe.command.help.TOPICS.items())]
+        longest_topic_len = max([len(name) for name,desc in topic_list])
+        for name,desc in topic_list:
+            extra_spaces = longest_topic_len - len(name)
+            ret.append('{}{}    {}'.format(name, ' '*extra_spaces, desc))
+
+        ret.extend(['', 'Run', '  be help [command|topic]',
+                    'for more information.'])
         return '\n'.join(ret)
 
     def version(self, *args):
@@ -275,12 +313,25 @@ def dispatch(ui, command, args):
                 'See http://docs.python.org/library/locale.html for details',
                 ])
         return 1
+    except libbe.command.UsageError, e:
+        print >> ui.io.stdout, 'Usage Error:\n', e
+        if e.command:
+            print >> ui.io.stdout, e.command.usage()
+        print >> ui.io.stdout, 'For usage information, try'
+        print >> ui.io.stdout, '  be help %s' % e.command_name
+        return 1
     except libbe.command.UserError, e:
         print >> ui.io.stdout, 'ERROR:\n', e
         return 1
+    except OSError, e:
+        print >> ui.io.stdout, 'OSError:\n', e
+        return 1
     except libbe.storage.ConnectionError, e:
         print >> ui.io.stdout, 'Connection Error:\n', e
         return 1
+    except libbe.util.http.HTTPError, e:
+        print >> ui.io.stdout, 'HTTP Error:\n', e
+        return 1
     except (libbe.util.id.MultipleIDMatches, libbe.util.id.NoIDMatches,
             libbe.util.id.InvalidIDStructure), e:
         print >> ui.io.stdout, 'Invalid id:\n', e
@@ -290,6 +341,7 @@ def dispatch(ui, command, args):
     return ret
 
 def main():
+    locale.setlocale(locale.LC_ALL, '')
     io = libbe.command.StdInputOutput()
     ui = CommandLine(io)
     be = BE(ui=ui)
@@ -300,15 +352,19 @@ def main():
         options,args = parser.parse_args()
     except CallbackExit:
         return 0
-    except libbe.command.UserError, e:
-        if str(e).endswith('COMMAND'):
+    except libbe.command.UsageError, e:
+        if isinstance(e.command, BE):
             # no command given, print usage string
-            print >> ui.io.stdout, 'ERROR:'
-            print >> ui.io.stdout, be.usage(), '\n', e
+            print >> ui.io.stdout, 'Usage Error:\n', e
+            print >> ui.io.stdout, be.usage()
             print >> ui.io.stdout, 'For example, try'
             print >> ui.io.stdout, '  be help'
         else:
-            print >> ui.io.stdout, 'ERROR:\n', e
+            print >> ui.io.stdout, 'Usage Error:\n', e
+            if e.command:
+                print >> ui.io.stdout, e.command.usage()
+            print >> ui.io.stdout, 'For usage information, try'
+            print >> ui.io.stdout, '  be help %s' % e.command_name
         return 1
 
     command_name = args.pop(0)
@@ -319,10 +375,12 @@ def main():
         return 1
 
     ui.storage_callbacks = libbe.command.StorageCallbacks(options['repo'])
-    command = Class(ui=ui)
+    command = Class(ui=ui, server=options['server'])
     ui.setup_command(command)
 
-    if command.name in ['comment', 'commit', 'import-xml', 'serve']:
+    if command.name in [
+        'new', 'comment', 'commit', 'html', 'import-xml', 'serve-storage',
+        'serve-commands']:
         paginate = 'never'
     else:
         paginate = 'auto'
@@ -333,7 +391,12 @@ def main():
     libbe.ui.util.pager.run_pager(paginate)
 
     ret = dispatch(ui, command, args)
-    ui.cleanup()
+    try:
+        ui.cleanup()
+    except IOError, e:
+        print >> ui.io.stdout, 'IOError:\n', e
+        return 1
+
     return ret
 
 if __name__ == '__main__':