diff options
-rwxr-xr-x | mic/cmd_chroot.py | 71 | ||||
-rwxr-xr-x | mic/cmd_create.py | 234 | ||||
-rw-r--r-- | mic/creator.py | 398 | ||||
-rwxr-xr-x | mic/helpformat.py | 90 | ||||
-rw-r--r-- | mic/utils/cmdln.py | 1586 | ||||
-rw-r--r-- | packaging/mic.spec | 5 | ||||
-rw-r--r-- | plugins/imager/fs_plugin.py | 19 | ||||
-rw-r--r-- | plugins/imager/loop_plugin.py | 29 | ||||
-rw-r--r-- | plugins/imager/qcow_plugin.py | 10 | ||||
-rwxr-xr-x | tools/mic | 394 |
10 files changed, 629 insertions, 2207 deletions
diff --git a/mic/cmd_chroot.py b/mic/cmd_chroot.py new file mode 100755 index 0000000..d7d76ef --- /dev/null +++ b/mic/cmd_chroot.py @@ -0,0 +1,71 @@ +#!/usr/bin/python -tt
+# vim: ai ts=4 sts=4 et sw=4
+#
+# Copyright (c) 2012 Intel, Inc.
+#
+# 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; version 2 of the License
+#
+# 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.
+#
+# 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., 59
+# Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Implementation of subcmd: chroot
+"""
+
+import os
+import os, sys, re
+import pwd
+import argparse
+
+from mic import msger
+from mic.utils import misc, errors
+from mic.conf import configmgr
+from mic.plugin import pluginmgr
+
+def _root_confirm():
+ """Make sure command is called by root
+ There are a lot of commands needed to be run during creating images,
+ some of them must be run with root privilege like mount, kpartx"""
+ if os.geteuid() != 0:
+ msger.error('Root permission is required to continue, abort')
+
+def main(parser, args, argv):
+ """mic choot entry point."""
+
+ #args is argparser namespace, argv is the input cmd line
+ if args is None:
+ raise errors.Usage("Invalid arguments")
+
+ targetimage = args.imagefile
+ if not os.path.exists(targetimage):
+ raise errors.CreatorError("Cannot find the image: %s"
+ % targetimage)
+
+ _root_confirm()
+
+ configmgr.chroot['saveto'] = args.saveto
+
+ imagetype = misc.get_image_type(targetimage)
+ if imagetype in ("ext3fsimg", "ext4fsimg", "btrfsimg"):
+ imagetype = "loop"
+
+ chrootclass = None
+ for pname, pcls in pluginmgr.get_plugins('imager').iteritems():
+ if pname == imagetype and hasattr(pcls, "do_chroot"):
+ chrootclass = pcls
+ break
+
+ if not chrootclass:
+ raise errors.CreatorError("Cannot support image type: %s" \
+ % imagetype)
+
+ chrootclass.do_chroot(targetimage, args.cmd)
+
+
diff --git a/mic/cmd_create.py b/mic/cmd_create.py new file mode 100755 index 0000000..ce574a1 --- /dev/null +++ b/mic/cmd_create.py @@ -0,0 +1,234 @@ +#!/usr/bin/python -tt
+# vim: ai ts=4 sts=4 et sw=4
+#
+# Copyright (c) 2012 Intel, Inc.
+#
+# 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; version 2 of the License
+#
+# 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.
+#
+# 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., 59
+# Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Implementation of subcmd: create
+"""
+
+import os
+import os, sys, re
+import pwd
+import argparse
+
+from mic import msger
+from mic.utils import errors, rpmmisc
+from mic.conf import configmgr
+from mic.plugin import pluginmgr
+
+def main(parser, args, argv):
+ """mic create entry point."""
+ #args is argparser namespace, argv is the input cmd line
+ if args is None:
+ raise errors.Usage("Invalid arguments")
+
+ if not os.path.exists(args.ksfile):
+ raise errors.CreatorError("Can't find the file: %s" % args.ksfile)
+
+ if os.geteuid() != 0:
+ msger.error("Root permission is required, abort")
+
+ try:
+ w = pwd.getpwuid(os.geteuid())
+ except KeyError:
+ msger.warning("Might fail in compressing stage for undetermined user")
+
+ abspath = lambda pth: os.path.abspath(os.path.expanduser(pth))
+ if args.logfile:
+ logfile_abs_path = abspath(args.logfile)
+ if os.path.isdir(logfile_abs_path):
+ raise errors.Usage("logfile's path %s should be file"
+ % args.logfile)
+ configmgr.create['logfile'] = logfile_abs_path
+ configmgr.set_logfile()
+
+ if args.subcommand == "auto":
+ do_auto(parser, args.ksfile, argv)
+ return
+
+ #check the imager type
+ createrClass = None
+ for subcmd, klass in pluginmgr.get_plugins('imager').iteritems():
+ if subcmd == args.subcommand and hasattr(klass, 'do_create'):
+ createrClass = klass
+
+ if createrClass is None:
+ raise errors.CreatorError("Can't support subcommand %s" % args.subcommand)
+
+ if args.config:
+ configmgr.reset()
+ configmgr._siteconf = args.config
+
+ if args.outdir is not None:
+ configmgr.create['outdir'] = abspath(args.outdir)
+ if args.cachedir is not None:
+ configmgr.create['cachedir'] = abspath(args.cachedir)
+ os.environ['ZYPP_LOCKFILE_ROOT'] = configmgr.create['cachedir']
+
+ for cdir in ('outdir', 'cachedir'):
+ if os.path.exists(configmgr.create[cdir]) \
+ and not os.path.isdir(configmgr.create[cdir]):
+ raise errors.Usage('Invalid directory specified: %s' \
+ % configmgr.create[cdir])
+ if not os.path.exists(configmgr.create[cdir]):
+ os.makedirs(configmgr.create[cdir])
+ if os.getenv('SUDO_UID', '') and os.getenv('SUDO_GID', ''):
+ os.chown(configmgr.create[cdir],
+ int(os.getenv('SUDO_UID')),
+ int(os.getenv('SUDO_GID')))
+
+ if args.local_pkgs_path is not None:
+ if not os.path.exists(args.local_pkgs_path):
+ raise errors.Usage('Local pkgs directory: \'%s\' not exist' \
+ % args.local_pkgs_path)
+ configmgr.create['local_pkgs_path'] = args.local_pkgs_path
+
+ if args.release:
+ configmgr.create['release'] = args.release.rstrip('/')
+
+ if args.record_pkgs:
+ configmgr.create['record_pkgs'] = []
+ for infotype in args.record_pkgs.split(','):
+ if infotype not in ('name', 'content', 'license', 'vcs'):
+ raise errors.Usage('Invalid pkg recording: %s, valid ones:'
+ ' "name", "content", "license", "vcs"' \
+ % infotype)
+
+ configmgr.create['record_pkgs'].append(infotype)
+
+ if args.strict_mode:
+ configmgr.create['strict_mode'] = args.strict_mode
+ if args.arch is not None:
+ supported_arch = sorted(rpmmisc.archPolicies.keys(), reverse=True)
+ if args.arch in supported_arch:
+ configmgr.create['arch'] = args.arch
+ else:
+ raise errors.Usage('Invalid architecture: "%s".\n'
+ ' Supported architectures are: \n'
+ ' %s' % (args.arch,
+ ', '.join(supported_arch)))
+
+ if args.pkgmgr is not None:
+ configmgr.create['pkgmgr'] = args.pkgmgr
+
+ if args.runtime:
+ configmgr.set_runtime(args.runtime)
+
+ if args.pack_to is not None:
+ configmgr.create['pack_to'] = args.pack_to
+
+ if args.copy_kernel:
+ configmgr.create['copy_kernel'] = args.copy_kernel
+
+ if args.install_pkgs:
+ configmgr.create['install_pkgs'] = []
+ for pkgtype in args.install_pkgs.split(','):
+ if pkgtype not in ('source', 'debuginfo', 'debugsource'):
+ raise errors.Usage('Invalid parameter specified: "%s", '
+ 'valid values: source, debuginfo, '
+ 'debusource' % pkgtype)
+
+ configmgr.create['install_pkgs'].append(pkgtype)
+
+ if args.check_pkgs:
+ for pkg in args.check_pkgs.split(','):
+ configmgr.create['check_pkgs'].append(pkg)
+
+ if args.enabletmpfs:
+ configmgr.create['enabletmpfs'] = args.enabletmpfs
+
+ if args.repourl:
+ for item in args.repourl:
+ try:
+ key, val = item.split('=')
+ except:
+ continue
+ configmgr.create['repourl'][key] = val
+
+ if args.repo:
+ for optvalue in args.repo:
+ repo = {}
+ for item in optvalue.split(';'):
+ try:
+ key, val = item.split('=')
+ except:
+ continue
+ repo[key.strip()] = val.strip()
+ if 'name' in repo:
+ configmgr.create['extrarepos'][repo['name']] = repo
+
+ if args.ignore_ksrepo:
+ configmgr.create['ignore_ksrepo'] = args.ignore_ksrepo
+
+ creater = createrClass()
+ creater.do_create(args)
+
+def do_auto(parser, ksfile, argv):
+ """${cmd_name}: auto detect image type from magic header
+
+ Usage:
+ ${name} ${cmd_name} <ksfile>
+
+ ${cmd_option_list}
+ """
+ def parse_magic_line(re_str, pstr, ptype='mic'):
+ ptn = re.compile(re_str)
+ m = ptn.match(pstr)
+ if not m or not m.groups():
+ return None
+
+ inline_argv = m.group(1).strip()
+ if ptype == 'mic':
+ m2 = re.search('(?P<format>\w+)', inline_argv)
+ elif ptype == 'mic2':
+ m2 = re.search('(-f|--format(=)?)\s*(?P<format>\w+)',
+ inline_argv)
+ else:
+ return None
+
+ if m2:
+ cmdname = m2.group('format')
+ inline_argv = inline_argv.replace(m2.group(0), '')
+ return (cmdname, inline_argv)
+
+ return None
+
+ if not os.path.exists(ksfile):
+ raise errors.CreatorError("Can't find the file: %s" % ksfile)
+
+ with open(ksfile, 'r') as rf:
+ first_line = rf.readline()
+
+ mic_re = '^#\s*-\*-mic-options-\*-\s+(.*)\s+-\*-mic-options-\*-'
+ mic2_re = '^#\s*-\*-mic2-options-\*-\s+(.*)\s+-\*-mic2-options-\*-'
+
+ result = parse_magic_line(mic_re, first_line, 'mic') \
+ or parse_magic_line(mic2_re, first_line, 'mic2')
+ if not result:
+ raise errors.KsError("Invalid magic line in file: %s" % ksfile)
+
+ ksargv = ' '.join(result).split()
+
+ argv.remove("auto")
+ index = argv.index("create")
+ #insert the subcommand
+ argv.insert(index+1, ksargv[0])
+ options = argv + ksargv[1:]
+
+ args = parser.parse_args(options)
+
+ main(parser, args, options)
+
diff --git a/mic/creator.py b/mic/creator.py deleted file mode 100644 index e7c6633..0000000 --- a/mic/creator.py +++ /dev/null @@ -1,398 +0,0 @@ -#!/usr/bin/python -tt -# -# Copyright (c) 2011 Intel, Inc. -# -# 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; version 2 of the License -# -# 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. -# -# 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., 59 -# Temple Place - Suite 330, Boston, MA 02111-1307, USA. - -import os, sys, re -import pwd -from optparse import SUPPRESS_HELP - -from mic import msger -from mic.utils import cmdln, errors, rpmmisc -from mic.conf import configmgr -from mic.plugin import pluginmgr - - -class Creator(cmdln.Cmdln): - """${name}: create an image - - Usage: - ${name} SUBCOMMAND <ksfile> [OPTS] - - ${command_list} - ${option_list} - """ - - name = 'mic create(cr)' - - def __init__(self, *args, **kwargs): - cmdln.Cmdln.__init__(self, *args, **kwargs) - self._subcmds = [] - - # get cmds from pluginmgr - # mix-in do_subcmd interface - for subcmd, klass in pluginmgr.get_plugins('imager').iteritems(): - if not hasattr(klass, 'do_create'): - msger.warning("Unsurpport subcmd: %s" % subcmd) - continue - - func = getattr(klass, 'do_create') - setattr(self.__class__, "do_"+subcmd, func) - self._subcmds.append(subcmd) - - def get_optparser(self): - optparser = cmdln.CmdlnOptionParser(self) - optparser.add_option('-d', '--debug', action='store_true', - dest='debug', - help=SUPPRESS_HELP) - optparser.add_option('-v', '--verbose', action='store_true', - dest='verbose', - help=SUPPRESS_HELP) - optparser.add_option('', '--logfile', type='string', dest='logfile', - default=None, - help='Path of logfile') - optparser.add_option('-c', '--config', type='string', dest='config', - default=None, - help='Specify config file for mic') - optparser.add_option('-k', '--cachedir', type='string', action='store', - dest='cachedir', default=None, - help='Cache directory to store the downloaded') - optparser.add_option('-o', '--outdir', type='string', action='store', - dest='outdir', default=None, - help='Output directory') - optparser.add_option('-A', '--arch', type='string', dest='arch', - default=None, - help='Specify repo architecture') - optparser.add_option('', '--release', type='string', dest='release', - default=None, metavar='RID', - help='Generate a release of RID with all necessary' - ' files, when @BUILD_ID@ is contained in ' - 'kickstart file, it will be replaced by RID') - optparser.add_option("", "--record-pkgs", type="string", - dest="record_pkgs", default=None, - help='Record the info of installed packages, ' - 'multiple values can be specified which ' - 'joined by ",", valid values: "name", ' - '"content", "license", "vcs"') - optparser.add_option('', '--pkgmgr', type='string', dest='pkgmgr', - default=None, - help='Specify backend package manager') - optparser.add_option('', '--local-pkgs-path', type='string', - dest='local_pkgs_path', default=None, - help='Path for local pkgs(rpms) to be installed') - optparser.add_option('', '--runtime', type='string', - dest='runtime', default=None, - help='Specify runtime mode, avaiable: bootstrap') - # --taring-to is alias to --pack-to - optparser.add_option('', '--taring-to', type='string', - dest='pack_to', default=None, - help=SUPPRESS_HELP) - optparser.add_option('', '--pack-to', type='string', - dest='pack_to', default=None, - help='Pack the images together into the specified' - ' achive, extension supported: .zip, .tar, ' - '.tar.gz, .tar.bz2, etc. by default, .tar ' - 'will be used') - optparser.add_option('', '--copy-kernel', action='store_true', - dest='copy_kernel', - help='Copy kernel files from image /boot directory' - ' to the image output directory.') - optparser.add_option('', '--install-pkgs', type='string', action='store', - dest='install_pkgs', default=None, - help='Specify what type of packages to be installed,' - ' valid: source, debuginfo, debugsource') - optparser.add_option('', '--check-pkgs', type='string', action='store', - dest='check_pkgs', default=[], - help='Check if given packages would be installed, ' - 'packages should be separated by comma') - optparser.add_option('', '--tmpfs', action='store_true', dest='enabletmpfs', - help='Setup tmpdir as tmpfs to accelerate, experimental' - ' feature, use it if you have more than 4G memory') - optparser.add_option('', '--repourl', action='append', - dest='repourl', default=[], - help=SUPPRESS_HELP) - optparser.add_option('-R', '--repo', action='append', - dest='repo', default=[], - help=SUPPRESS_HELP) - optparser.add_option('', '--ignore-ksrepo', action='store_true', - dest='ignore_ksrepo', default=False, - help=SUPPRESS_HELP) - optparser.add_option('', '--strict-mode', action='store_true', - dest='strict_mode', default=False, - help='Abort creation of image, if there are some errors' - ' during rpm installation. ') - return optparser - - def preoptparse(self, argv): - optparser = self.get_optparser() - - largs = [] - rargs = [] - while argv: - arg = argv.pop(0) - - if arg in ('-h', '--help'): - rargs.append(arg) - - elif optparser.has_option(arg): - largs.append(arg) - - if optparser.get_option(arg).takes_value(): - try: - largs.append(argv.pop(0)) - except IndexError: - raise errors.Usage("option %s requires arguments" % arg) - - else: - if arg.startswith("--"): - if "=" in arg: - opt = arg.split("=")[0] - else: - opt = None - elif arg.startswith("-") and len(arg) > 2: - opt = arg[0:2] - else: - opt = None - - if opt and optparser.has_option(opt): - largs.append(arg) - else: - rargs.append(arg) - - return largs + rargs - - def postoptparse(self): - abspath = lambda pth: os.path.abspath(os.path.expanduser(pth)) - - if self.options.verbose: - msger.set_loglevel('VERBOSE') - if self.options.debug: - msger.set_loglevel('DEBUG') - - if self.options.logfile: - logfile_abs_path = abspath(self.options.logfile) - if os.path.isdir(logfile_abs_path): - raise errors.Usage("logfile's path %s should be file" - % self.options.logfile) - configmgr.create['logfile'] = logfile_abs_path - configmgr.set_logfile() - - if self.options.config: - configmgr.reset() - configmgr._siteconf = self.options.config - - if self.options.outdir is not None: - configmgr.create['outdir'] = abspath(self.options.outdir) - if self.options.cachedir is not None: - configmgr.create['cachedir'] = abspath(self.options.cachedir) - os.environ['ZYPP_LOCKFILE_ROOT'] = configmgr.create['cachedir'] - - for cdir in ('outdir', 'cachedir'): - if os.path.exists(configmgr.create[cdir]) \ - and not os.path.isdir(configmgr.create[cdir]): - raise errors.Usage('Invalid directory specified: %s' \ - % configmgr.create[cdir]) - if not os.path.exists(configmgr.create[cdir]): - os.makedirs(configmgr.create[cdir]) - if os.getenv('SUDO_UID', '') and os.getenv('SUDO_GID', ''): - os.chown(configmgr.create[cdir], - int(os.getenv('SUDO_UID')), - int(os.getenv('SUDO_GID'))) - - if self.options.local_pkgs_path is not None: - if not os.path.exists(self.options.local_pkgs_path): - raise errors.Usage('Local pkgs directory: \'%s\' not exist' \ - % self.options.local_pkgs_path) - configmgr.create['local_pkgs_path'] = self.options.local_pkgs_path - - if self.options.release: - configmgr.create['release'] = self.options.release.rstrip('/') - - if self.options.record_pkgs: - configmgr.create['record_pkgs'] = [] - for infotype in self.options.record_pkgs.split(','): - if infotype not in ('name', 'content', 'license', 'vcs'): - raise errors.Usage('Invalid pkg recording: %s, valid ones:' - ' "name", "content", "license", "vcs"' \ - % infotype) - - configmgr.create['record_pkgs'].append(infotype) - - if self.options.strict_mode: - configmgr.create['strict_mode'] = self.options.strict_mode - if self.options.arch is not None: - supported_arch = sorted(rpmmisc.archPolicies.keys(), reverse=True) - if self.options.arch in supported_arch: - configmgr.create['arch'] = self.options.arch - else: - raise errors.Usage('Invalid architecture: "%s".\n' - ' Supported architectures are: \n' - ' %s' % (self.options.arch, - ', '.join(supported_arch))) - - if self.options.pkgmgr is not None: - configmgr.create['pkgmgr'] = self.options.pkgmgr - - if self.options.runtime: - configmgr.set_runtime(self.options.runtime) - - if self.options.pack_to is not None: - configmgr.create['pack_to'] = self.options.pack_to - - if self.options.copy_kernel: - configmgr.create['copy_kernel'] = self.options.copy_kernel - - if self.options.install_pkgs: - configmgr.create['install_pkgs'] = [] - for pkgtype in self.options.install_pkgs.split(','): - if pkgtype not in ('source', 'debuginfo', 'debugsource'): - raise errors.Usage('Invalid parameter specified: "%s", ' - 'valid values: source, debuginfo, ' - 'debusource' % pkgtype) - - configmgr.create['install_pkgs'].append(pkgtype) - - if self.options.check_pkgs: - for pkg in self.options.check_pkgs.split(','): - configmgr.create['check_pkgs'].append(pkg) - - if self.options.enabletmpfs: - configmgr.create['enabletmpfs'] = self.options.enabletmpfs - - if self.options.repourl: - for item in self.options.repourl: - try: - key, val = item.split('=') - except: - continue - configmgr.create['repourl'][key] = val - - if self.options.repo: - for optvalue in self.options.repo: - repo = {} - for item in optvalue.split(';'): - try: - key, val = item.split('=') - except: - continue - repo[key.strip()] = val.strip() - if 'name' in repo: - configmgr.create['extrarepos'][repo['name']] = repo - - if self.options.ignore_ksrepo: - configmgr.create['ignore_ksrepo'] = self.options.ignore_ksrepo - - def main(self, argv=None): - if argv is None: - argv = sys.argv - else: - argv = argv[:] # don't modify caller's list - - self.optparser = self.get_optparser() - if self.optparser: - try: - argv = self.preoptparse(argv) - self.options, args = self.optparser.parse_args(argv) - - except cmdln.CmdlnUserError, ex: - msg = "%s: %s\nTry '%s help' for info.\n"\ - % (self.name, ex, self.name) - raise errors.Usage(msg) - - except cmdln.StopOptionProcessing, ex: - return 0 - else: - # optparser=None means no process for opts - self.options, args = None, argv[1:] - - if not args: - return self.emptyline() - - self.postoptparse() - - return self.cmd(args) - - def precmd(self, argv): # check help before cmd - - if '-h' in argv or '?' in argv or '--help' in argv or 'help' in argv: - return argv - - if len(argv) == 1: - return ['help', argv[0]] - - if os.geteuid() != 0: - msger.error("Root permission is required, abort") - - try: - w = pwd.getpwuid(os.geteuid()) - except KeyError: - msger.warning("Might fail in compressing stage for undetermined user") - - return argv - - def do_auto(self, subcmd, opts, *args): - """${cmd_name}: auto detect image type from magic header - - Usage: - ${name} ${cmd_name} <ksfile> - - ${cmd_option_list} - """ - def parse_magic_line(re_str, pstr, ptype='mic'): - ptn = re.compile(re_str) - m = ptn.match(pstr) - if not m or not m.groups(): - return None - - inline_argv = m.group(1).strip() - if ptype == 'mic': - m2 = re.search('(?P<format>\w+)', inline_argv) - elif ptype == 'mic2': - m2 = re.search('(-f|--format(=)?)\s*(?P<format>\w+)', - inline_argv) - else: - return None - - if m2: - cmdname = m2.group('format') - inline_argv = inline_argv.replace(m2.group(0), '') - return (cmdname, inline_argv) - - return None - - if len(args) != 1: - raise errors.Usage("Extra arguments given") - - if not os.path.exists(args[0]): - raise errors.CreatorError("Can't find the file: %s" % args[0]) - - with open(args[0], 'r') as rf: - first_line = rf.readline() - - mic_re = '^#\s*-\*-mic-options-\*-\s+(.*)\s+-\*-mic-options-\*-' - mic2_re = '^#\s*-\*-mic2-options-\*-\s+(.*)\s+-\*-mic2-options-\*-' - - result = parse_magic_line(mic_re, first_line, 'mic') \ - or parse_magic_line(mic2_re, first_line, 'mic2') - if not result: - raise errors.KsError("Invalid magic line in file: %s" % args[0]) - - if result[0] not in self._subcmds: - raise errors.KsError("Unsupport format '%s' in %s" - % (result[0], args[0])) - - argv = ' '.join(result + args).split() - self.main(argv) - diff --git a/mic/helpformat.py b/mic/helpformat.py new file mode 100755 index 0000000..e17f716 --- /dev/null +++ b/mic/helpformat.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python
+# vim: ai ts=4 sts=4 et sw=4
+#
+# Copyright (c) 2011 Intel, Inc.
+#
+# 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; version 2 of the License
+#
+# 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.
+#
+# 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., 59
+# Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Local additions to commandline parsing."""
+
+import os
+import re
+import functools
+
+from argparse import RawDescriptionHelpFormatter, ArgumentTypeError
+
+class MICHelpFormatter(RawDescriptionHelpFormatter):
+ """Changed default argparse help output by request from cmdln lovers."""
+
+ def __init__(self, *args, **kwargs):
+ super(MICHelpFormatter, self).__init__(*args, **kwargs)
+ self._aliases = {}
+
+ def add_argument(self, action):
+ """Collect aliases."""
+
+ if action.choices:
+ for item, parser in action.choices.iteritems():
+ self._aliases[str(item)] = parser.get_default('alias')
+
+ return super(MICHelpFormatter, self).add_argument(action)
+
+ def format_help(self):
+ """
+ There is no safe and documented way in argparse to reformat
+ help output through APIs as almost all of them are private,
+ so this method just parses the output and changes it.
+ """
+ result = []
+ subcomm = False
+ for line in super(MICHelpFormatter, self).format_help().split('\n'):
+ if line.strip().startswith('{'):
+ continue
+ if line.startswith('optional arguments:'):
+ line = 'Global Options:'
+ if line.startswith('usage:'):
+ line = "Usage: mic [GLOBAL-OPTS] SUBCOMMAND [OPTS]"
+ if subcomm:
+ match = re.match("[ ]+([^ ]+)[ ]+(.+)", line)
+ if match:
+ name, help_text = match.group(1), match.group(2)
+ alias = self._aliases.get(name) or ''
+ if alias:
+ alias = "(%s)" % alias
+ line = " %-22s%s" % ("%s %s" % (name, alias), help_text)
+ if line.strip().startswith('subcommands:'):
+ line = 'Subcommands:'
+ subcomm = True
+ result.append(line)
+ return '\n'.join(result)
+
+def subparser(func):
+ """Convenient decorator for subparsers."""
+ @functools.wraps(func)
+ def wrapper(parser):
+ """
+ Create subparser
+ Set first line of function's docstring as a help
+ and the rest of the lines as a description.
+ Set attribute 'module' of subparser to 'cmd'+first part of function name
+ """
+ splitted = func.__doc__.split('\n')
+ name = func.__name__.split('_')[0]
+ subpar = parser.add_parser(name, help=splitted[0],
+ description='\n'.join(splitted[1:]),
+ formatter_class=RawDescriptionHelpFormatter)
+ subpar.set_defaults(module="cmd_%s" % name)
+ return func(subpar)
+ return wrapper
+
diff --git a/mic/utils/cmdln.py b/mic/utils/cmdln.py deleted file mode 100644 index 3f4bb0f..0000000 --- a/mic/utils/cmdln.py +++ /dev/null @@ -1,1586 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2002-2007 ActiveState Software Inc. -# License: MIT (see LICENSE.txt for license details) -# Author: Trent Mick -# Home: http://trentm.com/projects/cmdln/ - -"""An improvement on Python's standard cmd.py module. - -As with cmd.py, this module provides "a simple framework for writing -line-oriented command intepreters." This module provides a 'RawCmdln' -class that fixes some design flaws in cmd.Cmd, making it more scalable -and nicer to use for good 'cvs'- or 'svn'-style command line interfaces -or simple shells. And it provides a 'Cmdln' class that add -optparse-based option processing. Basically you use it like this: - - import cmdln - - class MySVN(cmdln.Cmdln): - name = "svn" - - @cmdln.alias('stat', 'st') - @cmdln.option('-v', '--verbose', action='store_true' - help='print verbose information') - def do_status(self, subcmd, opts, *paths): - print "handle 'svn status' command" - - #... - - if __name__ == "__main__": - shell = MySVN() - retval = shell.main() - sys.exit(retval) - -See the README.txt or <http://trentm.com/projects/cmdln/> for more -details. -""" - -__version_info__ = (1, 1, 2) -__version__ = '.'.join(map(str, __version_info__)) - -import os -import sys -import re -import cmd -import optparse -from pprint import pprint -import sys - - - - -#---- globals - -LOOP_ALWAYS, LOOP_NEVER, LOOP_IF_EMPTY = range(3) - -# An unspecified optional argument when None is a meaningful value. -_NOT_SPECIFIED = ("Not", "Specified") - -# Pattern to match a TypeError message from a call that -# failed because of incorrect number of arguments (see -# Python/getargs.c). -_INCORRECT_NUM_ARGS_RE = re.compile( - r"(takes [\w ]+ )(\d+)( arguments? \()(\d+)( given\))") - - - -#---- exceptions - -class CmdlnError(Exception): - """A cmdln.py usage error.""" - def __init__(self, msg): - self.msg = msg - def __str__(self): - return self.msg - -class CmdlnUserError(Exception): - """An error by a user of a cmdln-based tool/shell.""" - pass - - - -#---- public methods and classes - -def alias(*aliases): - """Decorator to add aliases for Cmdln.do_* command handlers. - - Example: - class MyShell(cmdln.Cmdln): - @cmdln.alias("!", "sh") - def do_shell(self, argv): - #...implement 'shell' command - """ - def decorate(f): - if not hasattr(f, "aliases"): - f.aliases = [] - f.aliases += aliases - return f - return decorate - - -class RawCmdln(cmd.Cmd): - """An improved (on cmd.Cmd) framework for building multi-subcommand - scripts (think "svn" & "cvs") and simple shells (think "pdb" and - "gdb"). - - A simple example: - - import cmdln - - class MySVN(cmdln.RawCmdln): - name = "svn" - - @cmdln.aliases('stat', 'st') - def do_status(self, argv): - print "handle 'svn status' command" - - if __name__ == "__main__": - shell = MySVN() - retval = shell.main() - sys.exit(retval) - - See <http://trentm.com/projects/cmdln> for more information. - """ - name = None # if unset, defaults basename(sys.argv[0]) - prompt = None # if unset, defaults to self.name+"> " - version = None # if set, default top-level options include --version - - # Default messages for some 'help' command error cases. - # They are interpolated with one arg: the command. - nohelp = "no help on '%s'" - unknowncmd = "unknown command: '%s'" - - helpindent = '' # string with which to indent help output - - def __init__(self, completekey='tab', - stdin=None, stdout=None, stderr=None): - """Cmdln(completekey='tab', stdin=None, stdout=None, stderr=None) - - The optional argument 'completekey' is the readline name of a - completion key; it defaults to the Tab key. If completekey is - not None and the readline module is available, command completion - is done automatically. - - The optional arguments 'stdin', 'stdout' and 'stderr' specify - alternate input, output and error output file objects; if not - specified, sys.* are used. - - If 'stdout' but not 'stderr' is specified, stdout is used for - error output. This is to provide least surprise for users used - to only the 'stdin' and 'stdout' options with cmd.Cmd. - """ - import sys - if self.name is None: - self.name = os.path.basename(sys.argv[0]) - if self.prompt is None: - self.prompt = self.name+"> " - self._name_str = self._str(self.name) - self._prompt_str = self._str(self.prompt) - if stdin is not None: - self.stdin = stdin - else: - self.stdin = sys.stdin - if stdout is not None: - self.stdout = stdout - else: - self.stdout = sys.stdout - if stderr is not None: - self.stderr = stderr - elif stdout is not None: - self.stderr = stdout - else: - self.stderr = sys.stderr - self.cmdqueue = [] - self.completekey = completekey - self.cmdlooping = False - - def get_optparser(self): - """Hook for subclasses to set the option parser for the - top-level command/shell. - - This option parser is used retrieved and used by `.main()' to - handle top-level options. - - The default implements a single '-h|--help' option. Sub-classes - can return None to have no options at the top-level. Typically - an instance of CmdlnOptionParser should be returned. - """ - version = (self.version is not None - and "%s %s" % (self._name_str, self.version) - or None) - return CmdlnOptionParser(self, version=version) - - def postoptparse(self): - """Hook method executed just after `.main()' parses top-level - options. - - When called `self.options' holds the results of the option parse. - """ - pass - - def main(self, argv=None, loop=LOOP_NEVER): - """A possible mainline handler for a script, like so: - - import cmdln - class MyCmd(cmdln.Cmdln): - name = "mycmd" - ... - - if __name__ == "__main__": - MyCmd().main() - - By default this will use sys.argv to issue a single command to - 'MyCmd', then exit. The 'loop' argument can be use to control - interactive shell behaviour. - - Arguments: - "argv" (optional, default sys.argv) is the command to run. - It must be a sequence, where the first element is the - command name and subsequent elements the args for that - command. - "loop" (optional, default LOOP_NEVER) is a constant - indicating if a command loop should be started (i.e. an - interactive shell). Valid values (constants on this module): - LOOP_ALWAYS start loop and run "argv", if any - LOOP_NEVER run "argv" (or .emptyline()) and exit - LOOP_IF_EMPTY run "argv", if given, and exit; - otherwise, start loop - """ - if argv is None: - import sys - argv = sys.argv - else: - argv = argv[:] # don't modify caller's list - - self.optparser = self.get_optparser() - if self.optparser: # i.e. optparser=None means don't process for opts - try: - self.options, args = self.optparser.parse_args(argv[1:]) - except CmdlnUserError, ex: - msg = "%s: %s\nTry '%s help' for info.\n"\ - % (self.name, ex, self.name) - self.stderr.write(self._str(msg)) - self.stderr.flush() - return 1 - except StopOptionProcessing, ex: - return 0 - else: - self.options, args = None, argv[1:] - self.postoptparse() - - if loop == LOOP_ALWAYS: - if args: - self.cmdqueue.append(args) - return self.cmdloop() - elif loop == LOOP_NEVER: - if args: - return self.cmd(args) - else: - return self.emptyline() - elif loop == LOOP_IF_EMPTY: - if args: - return self.cmd(args) - else: - return self.cmdloop() - - def cmd(self, argv): - """Run one command and exit. - - "argv" is the arglist for the command to run. argv[0] is the - command to run. If argv is an empty list then the - 'emptyline' handler is run. - - Returns the return value from the command handler. - """ - assert isinstance(argv, (list, tuple)), \ - "'argv' is not a sequence: %r" % argv - retval = None - try: - argv = self.precmd(argv) - retval = self.onecmd(argv) - self.postcmd(argv) - except: - if not self.cmdexc(argv): - raise - retval = 1 - return retval - - def _str(self, s): - """Safely convert the given str/unicode to a string for printing.""" - try: - return str(s) - except UnicodeError: - #XXX What is the proper encoding to use here? 'utf-8' seems - # to work better than "getdefaultencoding" (usually - # 'ascii'), on OS X at least. - #import sys - #return s.encode(sys.getdefaultencoding(), "replace") - return s.encode("utf-8", "replace") - - def cmdloop(self, intro=None): - """Repeatedly issue a prompt, accept input, parse into an argv, and - dispatch (via .precmd(), .onecmd() and .postcmd()), passing them - the argv. In other words, start a shell. - - "intro" (optional) is a introductory message to print when - starting the command loop. This overrides the class - "intro" attribute, if any. - """ - self.cmdlooping = True - self.preloop() - if self.use_rawinput and self.completekey: - try: - import readline - self.old_completer = readline.get_completer() - readline.set_completer(self.complete) - readline.parse_and_bind(self.completekey+": complete") - except ImportError: - pass - try: - if intro is None: - intro = self.intro - if intro: - intro_str = self._str(intro) - self.stdout.write(intro_str+'\n') - self.stop = False - retval = None - while not self.stop: - if self.cmdqueue: - argv = self.cmdqueue.pop(0) - assert isinstance(argv, (list, tuple)), \ - "item on 'cmdqueue' is not a sequence: %r" % argv - else: - if self.use_rawinput: - try: - line = raw_input(self._prompt_str) - except EOFError: - line = 'EOF' - else: - self.stdout.write(self._prompt_str) - self.stdout.flush() - line = self.stdin.readline() - if not len(line): - line = 'EOF' - else: - line = line[:-1] # chop '\n' - argv = line2argv(line) - try: - argv = self.precmd(argv) - retval = self.onecmd(argv) - self.postcmd(argv) - except: - if not self.cmdexc(argv): - raise - retval = 1 - self.lastretval = retval - self.postloop() - finally: - if self.use_rawinput and self.completekey: - try: - import readline - readline.set_completer(self.old_completer) - except ImportError: - pass - self.cmdlooping = False - return retval - - def precmd(self, argv): - """Hook method executed just before the command argv is - interpreted, but after the input prompt is generated and issued. - - "argv" is the cmd to run. - - Returns an argv to run (i.e. this method can modify the command - to run). - """ - return argv - - def postcmd(self, argv): - """Hook method executed just after a command dispatch is finished. - - "argv" is the command that was run. - """ - pass - - def cmdexc(self, argv): - """Called if an exception is raised in any of precmd(), onecmd(), - or postcmd(). If True is returned, the exception is deemed to have - been dealt with. Otherwise, the exception is re-raised. - - The default implementation handles CmdlnUserError's, which - typically correspond to user error in calling commands (as - opposed to programmer error in the design of the script using - cmdln.py). - """ - import sys - type, exc, traceback = sys.exc_info() - if isinstance(exc, CmdlnUserError): - msg = "%s %s: %s\nTry '%s help %s' for info.\n"\ - % (self.name, argv[0], exc, self.name, argv[0]) - self.stderr.write(self._str(msg)) - self.stderr.flush() - return True - - def onecmd(self, argv): - if not argv: - return self.emptyline() - self.lastcmd = argv - cmdname = self._get_canonical_cmd_name(argv[0]) - if cmdname: - handler = self._get_cmd_handler(cmdname) - if handler: - return self._dispatch_cmd(handler, argv) - return self.default(argv) - - def _dispatch_cmd(self, handler, argv): - return handler(argv) - - def default(self, argv): - """Hook called to handle a command for which there is no handler. - - "argv" is the command and arguments to run. - - The default implementation writes and error message to stderr - and returns an error exit status. - - Returns a numeric command exit status. - """ - errmsg = self._str(self.unknowncmd % (argv[0],)) - if self.cmdlooping: - self.stderr.write(errmsg+"\n") - else: - self.stderr.write("%s: %s\nTry '%s help' for info.\n" - % (self._name_str, errmsg, self._name_str)) - self.stderr.flush() - return 1 - - def parseline(self, line): - # This is used by Cmd.complete (readline completer function) to - # massage the current line buffer before completion processing. - # We override to drop special '!' handling. - line = line.strip() - if not line: - return None, None, line - elif line[0] == '?': - line = 'help ' + line[1:] - i, n = 0, len(line) - while i < n and line[i] in self.identchars: i = i+1 - cmd, arg = line[:i], line[i:].strip() - return cmd, arg, line - - def helpdefault(self, cmd, known): - """Hook called to handle help on a command for which there is no - help handler. - - "cmd" is the command name on which help was requested. - "known" is a boolean indicating if this command is known - (i.e. if there is a handler for it). - - Returns a return code. - """ - if known: - msg = self._str(self.nohelp % (cmd,)) - if self.cmdlooping: - self.stderr.write(msg + '\n') - else: - self.stderr.write("%s: %s\n" % (self.name, msg)) - else: - msg = self.unknowncmd % (cmd,) - if self.cmdlooping: - self.stderr.write(msg + '\n') - else: - self.stderr.write("%s: %s\n" - "Try '%s help' for info.\n" - % (self.name, msg, self.name)) - self.stderr.flush() - return 1 - - def do_help(self, argv): - """${cmd_name}: give detailed help on a specific sub-command - - Usage: - ${name} help [COMMAND] - """ - if len(argv) > 1: # asking for help on a particular command - doc = None - cmdname = self._get_canonical_cmd_name(argv[1]) or argv[1] - if not cmdname: - return self.helpdefault(argv[1], False) - else: - helpfunc = getattr(self, "help_"+cmdname, None) - if helpfunc: - doc = helpfunc() - else: - handler = self._get_cmd_handler(cmdname) - if handler: - doc = handler.__doc__ - if doc is None: - return self.helpdefault(argv[1], handler != None) - else: # bare "help" command - doc = self.__class__.__doc__ # try class docstring - if doc is None: - # Try to provide some reasonable useful default help. - if self.cmdlooping: prefix = "" - else: prefix = self.name+' ' - doc = """Usage: - %sCOMMAND [ARGS...] - %shelp [COMMAND] - - ${option_list} - ${command_list} - ${help_list} - """ % (prefix, prefix) - cmdname = None - - if doc: # *do* have help content, massage and print that - doc = self.help_reindent(doc) - doc = self.help_preprocess(doc, cmdname) - doc = doc.rstrip() + '\n' # trim down trailing space - self.stdout.write(self._str(doc)) - self.stdout.flush() - do_help.aliases = ["?"] - - def help_reindent(self, help, indent=None): - """Hook to re-indent help strings before writing to stdout. - - "help" is the help content to re-indent - "indent" is a string with which to indent each line of the - help content after normalizing. If unspecified or None - then the default is use: the 'self.helpindent' class - attribute. By default this is the empty string, i.e. - no indentation. - - By default, all common leading whitespace is removed and then - the lot is indented by 'self.helpindent'. When calculating the - common leading whitespace the first line is ignored -- hence - help content for Conan can be written as follows and have the - expected indentation: - - def do_crush(self, ...): - '''${cmd_name}: crush your enemies, see them driven before you... - - c.f. Conan the Barbarian''' - """ - if indent is None: - indent = self.helpindent - lines = help.splitlines(0) - _dedentlines(lines, skip_first_line=True) - lines = [(indent+line).rstrip() for line in lines] - return '\n'.join(lines) - - def help_preprocess(self, help, cmdname): - """Hook to preprocess a help string before writing to stdout. - - "help" is the help string to process. - "cmdname" is the canonical sub-command name for which help - is being given, or None if the help is not specific to a - command. - - By default the following template variables are interpolated in - help content. (Note: these are similar to Python 2.4's - string.Template interpolation but not quite.) - - ${name} - The tool's/shell's name, i.e. 'self.name'. - ${option_list} - A formatted table of options for this shell/tool. - ${command_list} - A formatted table of available sub-commands. - ${help_list} - A formatted table of additional help topics (i.e. 'help_*' - methods with no matching 'do_*' method). - ${cmd_name} - The name (and aliases) for this sub-command formatted as: - "NAME (ALIAS1, ALIAS2, ...)". - ${cmd_usage} - A formatted usage block inferred from the command function - signature. - ${cmd_option_list} - A formatted table of options for this sub-command. (This is - only available for commands using the optparse integration, - i.e. using @cmdln.option decorators or manually setting the - 'optparser' attribute on the 'do_*' method.) - - Returns the processed help. - """ - preprocessors = { - "${name}": self._help_preprocess_name, - "${option_list}": self._help_preprocess_option_list, - "${command_list}": self._help_preprocess_command_list, - "${help_list}": self._help_preprocess_help_list, - "${cmd_name}": self._help_preprocess_cmd_name, - "${cmd_usage}": self._help_preprocess_cmd_usage, - "${cmd_option_list}": self._help_preprocess_cmd_option_list, - } - - for marker, preprocessor in preprocessors.items(): - if marker in help: - help = preprocessor(help, cmdname) - return help - - def _help_preprocess_name(self, help, cmdname=None): - return help.replace("${name}", self.name) - - def _help_preprocess_option_list(self, help, cmdname=None): - marker = "${option_list}" - indent, indent_width = _get_indent(marker, help) - suffix = _get_trailing_whitespace(marker, help) - - if self.optparser: - # Setup formatting options and format. - # - Indentation of 4 is better than optparse default of 2. - # C.f. Damian Conway's discussion of this in Perl Best - # Practices. - self.optparser.formatter.indent_increment = 4 - self.optparser.formatter.current_indent = indent_width - block = self.optparser.format_option_help() + '\n' - else: - block = "" - - help = help.replace(indent+marker+suffix, block, 1) - return help - - - def _help_preprocess_command_list(self, help, cmdname=None): - marker = "${command_list}" - indent, indent_width = _get_indent(marker, help) - suffix = _get_trailing_whitespace(marker, help) - - # Find any aliases for commands. - token2canonical = self._get_canonical_map() - aliases = {} - for token, cmdname in token2canonical.items(): - if token == cmdname: continue - aliases.setdefault(cmdname, []).append(token) - - # Get the list of (non-hidden) commands and their - # documentation, if any. - cmdnames = {} # use a dict to strip duplicates - for attr in self.get_names(): - if attr.startswith("do_"): - cmdnames[attr[3:]] = True - cmdnames = cmdnames.keys() - cmdnames.sort() - linedata = [] - for cmdname in cmdnames: - if aliases.get(cmdname): - a = aliases[cmdname] - a.sort() - cmdstr = "%s (%s)" % (cmdname, ", ".join(a)) - else: - cmdstr = cmdname - doc = None - try: - helpfunc = getattr(self, 'help_'+cmdname) - except AttributeError: - handler = self._get_cmd_handler(cmdname) - if handler: - doc = handler.__doc__ - else: - doc = helpfunc() - - # Strip "${cmd_name}: " from the start of a command's doc. Best - # practice dictates that command help strings begin with this, but - # it isn't at all wanted for the command list. - to_strip = "${cmd_name}:" - if doc and doc.startswith(to_strip): - #log.debug("stripping %r from start of %s's help string", - # to_strip, cmdname) - doc = doc[len(to_strip):].lstrip() - linedata.append( (cmdstr, doc) ) - - if linedata: - subindent = indent + ' '*4 - lines = _format_linedata(linedata, subindent, indent_width+4) - block = indent + "Commands:\n" \ - + '\n'.join(lines) + "\n\n" - help = help.replace(indent+marker+suffix, block, 1) - return help - - def _gen_names_and_attrs(self): - # Inheritance says we have to look in class and - # base classes; order is not important. - names = [] - classes = [self.__class__] - while classes: - aclass = classes.pop(0) - if aclass.__bases__: - classes = classes + list(aclass.__bases__) - for name in dir(aclass): - yield (name, getattr(aclass, name)) - - def _help_preprocess_help_list(self, help, cmdname=None): - marker = "${help_list}" - indent, indent_width = _get_indent(marker, help) - suffix = _get_trailing_whitespace(marker, help) - - # Determine the additional help topics, if any. - helpnames = {} - token2cmdname = self._get_canonical_map() - for attrname, attr in self._gen_names_and_attrs(): - if not attrname.startswith("help_"): continue - helpname = attrname[5:] - if helpname not in token2cmdname: - helpnames[helpname] = attr - - if helpnames: - linedata = [(n, a.__doc__ or "") for n, a in helpnames.items()] - linedata.sort() - - subindent = indent + ' '*4 - lines = _format_linedata(linedata, subindent, indent_width+4) - block = (indent - + "Additional help topics (run `%s help TOPIC'):\n" % self.name - + '\n'.join(lines) - + "\n\n") - else: - block = '' - help = help.replace(indent+marker+suffix, block, 1) - return help - - def _help_preprocess_cmd_name(self, help, cmdname=None): - marker = "${cmd_name}" - handler = self._get_cmd_handler(cmdname) - if not handler: - raise CmdlnError("cannot preprocess '%s' into help string: " - "could not find command handler for %r" - % (marker, cmdname)) - s = cmdname - if hasattr(handler, "aliases"): - s += " (%s)" % (", ".join(handler.aliases)) - help = help.replace(marker, s) - return help - - #TODO: this only makes sense as part of the Cmdln class. - # Add hooks to add help preprocessing template vars and put - # this one on that class. - def _help_preprocess_cmd_usage(self, help, cmdname=None): - marker = "${cmd_usage}" - handler = self._get_cmd_handler(cmdname) - if not handler: - raise CmdlnError("cannot preprocess '%s' into help string: " - "could not find command handler for %r" - % (marker, cmdname)) - indent, indent_width = _get_indent(marker, help) - suffix = _get_trailing_whitespace(marker, help) - - # Extract the introspection bits we need. - func = handler.im_func - if func.func_defaults: - func_defaults = list(func.func_defaults) - else: - func_defaults = [] - co_argcount = func.func_code.co_argcount - co_varnames = func.func_code.co_varnames - co_flags = func.func_code.co_flags - CO_FLAGS_ARGS = 4 - CO_FLAGS_KWARGS = 8 - - # Adjust argcount for possible *args and **kwargs arguments. - argcount = co_argcount - if co_flags & CO_FLAGS_ARGS: argcount += 1 - if co_flags & CO_FLAGS_KWARGS: argcount += 1 - - # Determine the usage string. - usage = "%s %s" % (self.name, cmdname) - if argcount <= 2: # handler ::= do_FOO(self, argv) - usage += " [ARGS...]" - elif argcount >= 3: # handler ::= do_FOO(self, subcmd, opts, ...) - argnames = list(co_varnames[3:argcount]) - tail = "" - if co_flags & CO_FLAGS_KWARGS: - name = argnames.pop(-1) - import warnings - # There is no generally accepted mechanism for passing - # keyword arguments from the command line. Could - # *perhaps* consider: arg=value arg2=value2 ... - warnings.warn("argument '**%s' on '%s.%s' command " - "handler will never get values" - % (name, self.__class__.__name__, - func.func_name)) - if co_flags & CO_FLAGS_ARGS: - name = argnames.pop(-1) - tail = "[%s...]" % name.upper() - while func_defaults: - func_defaults.pop(-1) - name = argnames.pop(-1) - tail = "[%s%s%s]" % (name.upper(), (tail and ' ' or ''), tail) - while argnames: - name = argnames.pop(-1) - tail = "%s %s" % (name.upper(), tail) - usage += ' ' + tail - - block_lines = [ - self.helpindent + "Usage:", - self.helpindent + ' '*4 + usage - ] - block = '\n'.join(block_lines) + '\n\n' - - help = help.replace(indent+marker+suffix, block, 1) - return help - - #TODO: this only makes sense as part of the Cmdln class. - # Add hooks to add help preprocessing template vars and put - # this one on that class. - def _help_preprocess_cmd_option_list(self, help, cmdname=None): - marker = "${cmd_option_list}" - handler = self._get_cmd_handler(cmdname) - if not handler: - raise CmdlnError("cannot preprocess '%s' into help string: " - "could not find command handler for %r" - % (marker, cmdname)) - indent, indent_width = _get_indent(marker, help) - suffix = _get_trailing_whitespace(marker, help) - if hasattr(handler, "optparser"): - # Setup formatting options and format. - # - Indentation of 4 is better than optparse default of 2. - # C.f. Damian Conway's discussion of this in Perl Best - # Practices. - handler.optparser.formatter.indent_increment = 4 - handler.optparser.formatter.current_indent = indent_width - block = handler.optparser.format_option_help() + '\n' - else: - block = "" - - help = help.replace(indent+marker+suffix, block, 1) - return help - - def _get_canonical_cmd_name(self, token): - map = self._get_canonical_map() - return map.get(token, None) - - def _get_canonical_map(self): - """Return a mapping of available command names and aliases to - their canonical command name. - """ - cacheattr = "_token2canonical" - if not hasattr(self, cacheattr): - # Get the list of commands and their aliases, if any. - token2canonical = {} - cmd2funcname = {} # use a dict to strip duplicates - for attr in self.get_names(): - if attr.startswith("do_"): cmdname = attr[3:] - elif attr.startswith("_do_"): cmdname = attr[4:] - else: - continue - cmd2funcname[cmdname] = attr - token2canonical[cmdname] = cmdname - for cmdname, funcname in cmd2funcname.items(): # add aliases - func = getattr(self, funcname) - aliases = getattr(func, "aliases", []) - for alias in aliases: - if alias in cmd2funcname: - import warnings - warnings.warn("'%s' alias for '%s' command conflicts " - "with '%s' handler" - % (alias, cmdname, cmd2funcname[alias])) - continue - token2canonical[alias] = cmdname - setattr(self, cacheattr, token2canonical) - return getattr(self, cacheattr) - - def _get_cmd_handler(self, cmdname): - handler = None - try: - handler = getattr(self, 'do_' + cmdname) - except AttributeError: - try: - # Private command handlers begin with "_do_". - handler = getattr(self, '_do_' + cmdname) - except AttributeError: - pass - return handler - - def _do_EOF(self, argv): - # Default EOF handler - # Note: an actual EOF is redirected to this command. - #TODO: separate name for this. Currently it is available from - # command-line. Is that okay? - self.stdout.write('\n') - self.stdout.flush() - self.stop = True - - def emptyline(self): - # Different from cmd.Cmd: don't repeat the last command for an - # emptyline. - if self.cmdlooping: - pass - else: - return self.do_help(["help"]) - - -#---- optparse.py extension to fix (IMO) some deficiencies -# -# See the class _OptionParserEx docstring for details. -# - -class StopOptionProcessing(Exception): - """Indicate that option *and argument* processing should stop - cleanly. This is not an error condition. It is similar in spirit to - StopIteration. This is raised by _OptionParserEx's default "help" - and "version" option actions and can be raised by custom option - callbacks too. - - Hence the typical CmdlnOptionParser (a subclass of _OptionParserEx) - usage is: - - parser = CmdlnOptionParser(mycmd) - parser.add_option("-f", "--force", dest="force") - ... - try: - opts, args = parser.parse_args() - except StopOptionProcessing: - # normal termination, "--help" was probably given - sys.exit(0) - """ - -class _OptionParserEx(optparse.OptionParser): - """An optparse.OptionParser that uses exceptions instead of sys.exit. - - This class is an extension of optparse.OptionParser that differs - as follows: - - Correct (IMO) the default OptionParser error handling to never - sys.exit(). Instead OptParseError exceptions are passed through. - - Add the StopOptionProcessing exception (a la StopIteration) to - indicate normal termination of option processing. - See StopOptionProcessing's docstring for details. - - I'd also like to see the following in the core optparse.py, perhaps - as a RawOptionParser which would serve as a base class for the more - generally used OptionParser (that works as current): - - Remove the implicit addition of the -h|--help and --version - options. They can get in the way (e.g. if want '-?' and '-V' for - these as well) and it is not hard to do: - optparser.add_option("-h", "--help", action="help") - optparser.add_option("--version", action="version") - These are good practices, just not valid defaults if they can - get in the way. - """ - def error(self, msg): - raise optparse.OptParseError(msg) - - def exit(self, status=0, msg=None): - if status == 0: - raise StopOptionProcessing(msg) - else: - #TODO: don't lose status info here - raise optparse.OptParseError(msg) - - - -#---- optparse.py-based option processing support - -class CmdlnOptionParser(_OptionParserEx): - """An optparse.OptionParser class more appropriate for top-level - Cmdln options. For parsing of sub-command options, see - SubCmdOptionParser. - - Changes: - - disable_interspersed_args() by default, because a Cmdln instance - has sub-commands which may themselves have options. - - Redirect print_help() to the Cmdln.do_help() which is better - equiped to handle the "help" action. - - error() will raise a CmdlnUserError: OptionParse.error() is meant - to be called for user errors. Raising a well-known error here can - make error handling clearer. - - Also see the changes in _OptionParserEx. - """ - def __init__(self, cmdln, **kwargs): - self.cmdln = cmdln - kwargs["prog"] = self.cmdln.name - _OptionParserEx.__init__(self, **kwargs) - self.disable_interspersed_args() - - def print_help(self, file=None): - self.cmdln.onecmd(["help"]) - - def error(self, msg): - raise CmdlnUserError(msg) - - -class SubCmdOptionParser(_OptionParserEx): - def set_cmdln_info(self, cmdln, subcmd): - """Called by Cmdln to pass relevant info about itself needed - for print_help(). - """ - self.cmdln = cmdln - self.subcmd = subcmd - - def print_help(self, file=None): - self.cmdln.onecmd(["help", self.subcmd]) - - def error(self, msg): - raise CmdlnUserError(msg) - - -def option(*args, **kwargs): - """Decorator to add an option to the optparser argument of a Cmdln - subcommand. - - Example: - class MyShell(cmdln.Cmdln): - @cmdln.option("-f", "--force", help="force removal") - def do_remove(self, subcmd, opts, *args): - #... - """ - #XXX Is there a possible optimization for many options to not have a - # large stack depth here? - def decorate(f): - if not hasattr(f, "optparser"): - f.optparser = SubCmdOptionParser() - f.optparser.add_option(*args, **kwargs) - return f - return decorate - - -class Cmdln(RawCmdln): - """An improved (on cmd.Cmd) framework for building multi-subcommand - scripts (think "svn" & "cvs") and simple shells (think "pdb" and - "gdb"). - - A simple example: - - import cmdln - - class MySVN(cmdln.Cmdln): - name = "svn" - - @cmdln.aliases('stat', 'st') - @cmdln.option('-v', '--verbose', action='store_true' - help='print verbose information') - def do_status(self, subcmd, opts, *paths): - print "handle 'svn status' command" - - #... - - if __name__ == "__main__": - shell = MySVN() - retval = shell.main() - sys.exit(retval) - - 'Cmdln' extends 'RawCmdln' by providing optparse option processing - integration. See this class' _dispatch_cmd() docstring and - <http://trentm.com/projects/cmdln> for more information. - """ - def _dispatch_cmd(self, handler, argv): - """Introspect sub-command handler signature to determine how to - dispatch the command. The raw handler provided by the base - 'RawCmdln' class is still supported: - - def do_foo(self, argv): - # 'argv' is the vector of command line args, argv[0] is - # the command name itself (i.e. "foo" or an alias) - pass - - In addition, if the handler has more than 2 arguments option - processing is automatically done (using optparse): - - @cmdln.option('-v', '--verbose', action='store_true') - def do_bar(self, subcmd, opts, *args): - # subcmd = <"bar" or an alias> - # opts = <an optparse.Values instance> - if opts.verbose: - print "lots of debugging output..." - # args = <tuple of arguments> - for arg in args: - bar(arg) - - TODO: explain that "*args" can be other signatures as well. - - The `cmdln.option` decorator corresponds to an `add_option()` - method call on an `optparse.OptionParser` instance. - - You can declare a specific number of arguments: - - @cmdln.option('-v', '--verbose', action='store_true') - def do_bar2(self, subcmd, opts, bar_one, bar_two): - #... - - and an appropriate error message will be raised/printed if the - command is called with a different number of args. - """ - co_argcount = handler.im_func.func_code.co_argcount - if co_argcount == 2: # handler ::= do_foo(self, argv) - return handler(argv) - elif co_argcount >= 3: # handler ::= do_foo(self, subcmd, opts, ...) - try: - optparser = handler.optparser - except AttributeError: - optparser = handler.im_func.optparser = SubCmdOptionParser() - assert isinstance(optparser, SubCmdOptionParser) - optparser.set_cmdln_info(self, argv[0]) - try: - opts, args = optparser.parse_args(argv[1:]) - except StopOptionProcessing: - #TODO: this doesn't really fly for a replacement of - # optparse.py behaviour, does it? - return 0 # Normal command termination - - try: - return handler(argv[0], opts, *args) - except TypeError, ex: - # Some TypeError's are user errors: - # do_foo() takes at least 4 arguments (3 given) - # do_foo() takes at most 5 arguments (6 given) - # do_foo() takes exactly 5 arguments (6 given) - # Raise CmdlnUserError for these with a suitably - # massaged error message. - import sys - tb = sys.exc_info()[2] # the traceback object - if tb.tb_next is not None: - # If the traceback is more than one level deep, then the - # TypeError do *not* happen on the "handler(...)" call - # above. In that we don't want to handle it specially - # here: it would falsely mask deeper code errors. - raise - msg = ex.args[0] - match = _INCORRECT_NUM_ARGS_RE.search(msg) - if match: - msg = list(match.groups()) - msg[1] = int(msg[1]) - 3 - if msg[1] == 1: - msg[2] = msg[2].replace("arguments", "argument") - msg[3] = int(msg[3]) - 3 - msg = ''.join(map(str, msg)) - raise CmdlnUserError(msg) - else: - raise - else: - raise CmdlnError("incorrect argcount for %s(): takes %d, must " - "take 2 for 'argv' signature or 3+ for 'opts' " - "signature" % (handler.__name__, co_argcount)) - - - -#---- internal support functions - -def _format_linedata(linedata, indent, indent_width): - """Format specific linedata into a pleasant layout. - - "linedata" is a list of 2-tuples of the form: - (<item-display-string>, <item-docstring>) - "indent" is a string to use for one level of indentation - "indent_width" is a number of columns by which the - formatted data will be indented when printed. - - The <item-display-string> column is held to 15 columns. - """ - lines = [] - WIDTH = 78 - indent_width - SPACING = 2 - NAME_WIDTH_LOWER_BOUND = 13 - NAME_WIDTH_UPPER_BOUND = 16 - NAME_WIDTH = max([len(s) for s,d in linedata]) - if NAME_WIDTH < NAME_WIDTH_LOWER_BOUND: - NAME_WIDTH = NAME_WIDTH_LOWER_BOUND - else: - NAME_WIDTH = NAME_WIDTH_UPPER_BOUND - - DOC_WIDTH = WIDTH - NAME_WIDTH - SPACING - for namestr, doc in linedata: - line = indent + namestr - if len(namestr) <= NAME_WIDTH: - line += ' ' * (NAME_WIDTH + SPACING - len(namestr)) - else: - lines.append(line) - line = indent + ' ' * (NAME_WIDTH + SPACING) - line += _summarize_doc(doc, DOC_WIDTH) - lines.append(line.rstrip()) - return lines - -def _summarize_doc(doc, length=60): - r"""Parse out a short one line summary from the given doclines. - - "doc" is the doc string to summarize. - "length" is the max length for the summary - - >>> _summarize_doc("this function does this") - 'this function does this' - >>> _summarize_doc("this function does this", 10) - 'this fu...' - >>> _summarize_doc("this function does this\nand that") - 'this function does this and that' - >>> _summarize_doc("this function does this\n\nand that") - 'this function does this' - """ - import re - if doc is None: - return "" - assert length > 3, "length <= 3 is absurdly short for a doc summary" - doclines = doc.strip().splitlines(0) - if not doclines: - return "" - - summlines = [] - for i, line in enumerate(doclines): - stripped = line.strip() - if not stripped: - break - summlines.append(stripped) - if len(''.join(summlines)) >= length: - break - - summary = ' '.join(summlines) - if len(summary) > length: - summary = summary[:length-3] + "..." - return summary - - -def line2argv(line): - r"""Parse the given line into an argument vector. - - "line" is the line of input to parse. - - This may get niggly when dealing with quoting and escaping. The - current state of this parsing may not be completely thorough/correct - in this respect. - - >>> from cmdln import line2argv - >>> line2argv("foo") - ['foo'] - >>> line2argv("foo bar") - ['foo', 'bar'] - >>> line2argv("foo bar ") - ['foo', 'bar'] - >>> line2argv(" foo bar") - ['foo', 'bar'] - - Quote handling: - - >>> line2argv("'foo bar'") - ['foo bar'] - >>> line2argv('"foo bar"') - ['foo bar'] - >>> line2argv(r'"foo\"bar"') - ['foo"bar'] - >>> line2argv("'foo bar' spam") - ['foo bar', 'spam'] - >>> line2argv("'foo 'bar spam") - ['foo bar', 'spam'] - - >>> line2argv('some\tsimple\ttests') - ['some', 'simple', 'tests'] - >>> line2argv('a "more complex" test') - ['a', 'more complex', 'test'] - >>> line2argv('a more="complex test of " quotes') - ['a', 'more=complex test of ', 'quotes'] - >>> line2argv('a more" complex test of " quotes') - ['a', 'more complex test of ', 'quotes'] - >>> line2argv('an "embedded \\"quote\\""') - ['an', 'embedded "quote"'] - - # Komodo bug 48027 - >>> line2argv('foo bar C:\\') - ['foo', 'bar', 'C:\\'] - - # Komodo change 127581 - >>> line2argv(r'"\test\slash" "foo bar" "foo\"bar"') - ['\\test\\slash', 'foo bar', 'foo"bar'] - - # Komodo change 127629 - >>> if sys.platform == "win32": - ... line2argv(r'\foo\bar') == ['\\foo\\bar'] - ... line2argv(r'\\foo\\bar') == ['\\\\foo\\\\bar'] - ... line2argv('"foo') == ['foo'] - ... else: - ... line2argv(r'\foo\bar') == ['foobar'] - ... line2argv(r'\\foo\\bar') == ['\\foo\\bar'] - ... try: - ... line2argv('"foo') - ... except ValueError, ex: - ... "not terminated" in str(ex) - True - True - True - """ - import string - line = line.strip() - argv = [] - state = "default" - arg = None # the current argument being parsed - i = -1 - while 1: - i += 1 - if i >= len(line): break - ch = line[i] - - if ch == "\\" and i+1 < len(line): - # escaped char always added to arg, regardless of state - if arg is None: arg = "" - if (sys.platform == "win32" - or state in ("double-quoted", "single-quoted") - ) and line[i+1] not in tuple('"\''): - arg += ch - i += 1 - arg += line[i] - continue - - if state == "single-quoted": - if ch == "'": - state = "default" - else: - arg += ch - elif state == "double-quoted": - if ch == '"': - state = "default" - else: - arg += ch - elif state == "default": - if ch == '"': - if arg is None: arg = "" - state = "double-quoted" - elif ch == "'": - if arg is None: arg = "" - state = "single-quoted" - elif ch in string.whitespace: - if arg is not None: - argv.append(arg) - arg = None - else: - if arg is None: arg = "" - arg += ch - if arg is not None: - argv.append(arg) - if not sys.platform == "win32" and state != "default": - raise ValueError("command line is not terminated: unfinished %s " - "segment" % state) - return argv - - -def argv2line(argv): - r"""Put together the given argument vector into a command line. - - "argv" is the argument vector to process. - - >>> from cmdln import argv2line - >>> argv2line(['foo']) - 'foo' - >>> argv2line(['foo', 'bar']) - 'foo bar' - >>> argv2line(['foo', 'bar baz']) - 'foo "bar baz"' - >>> argv2line(['foo"bar']) - 'foo"bar' - >>> print argv2line(['foo" bar']) - 'foo" bar' - >>> print argv2line(["foo' bar"]) - "foo' bar" - >>> argv2line(["foo'bar"]) - "foo'bar" - """ - escapedArgs = [] - for arg in argv: - if ' ' in arg and '"' not in arg: - arg = '"'+arg+'"' - elif ' ' in arg and "'" not in arg: - arg = "'"+arg+"'" - elif ' ' in arg: - arg = arg.replace('"', r'\"') - arg = '"'+arg+'"' - escapedArgs.append(arg) - return ' '.join(escapedArgs) - - -# Recipe: dedent (0.1) in /Users/trentm/tm/recipes/cookbook -def _dedentlines(lines, tabsize=8, skip_first_line=False): - """_dedentlines(lines, tabsize=8, skip_first_line=False) -> dedented lines - - "lines" is a list of lines to dedent. - "tabsize" is the tab width to use for indent width calculations. - "skip_first_line" is a boolean indicating if the first line should - be skipped for calculating the indent width and for dedenting. - This is sometimes useful for docstrings and similar. - - Same as dedent() except operates on a sequence of lines. Note: the - lines list is modified **in-place**. - """ - DEBUG = False - if DEBUG: - print "dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\ - % (tabsize, skip_first_line) - indents = [] - margin = None - for i, line in enumerate(lines): - if i == 0 and skip_first_line: continue - indent = 0 - for ch in line: - if ch == ' ': - indent += 1 - elif ch == '\t': - indent += tabsize - (indent % tabsize) - elif ch in '\r\n': - continue # skip all-whitespace lines - else: - break - else: - continue # skip all-whitespace lines - if DEBUG: print "dedent: indent=%d: %r" % (indent, line) - if margin is None: - margin = indent - else: - margin = min(margin, indent) - if DEBUG: print "dedent: margin=%r" % margin - - if margin is not None and margin > 0: - for i, line in enumerate(lines): - if i == 0 and skip_first_line: continue - removed = 0 - for j, ch in enumerate(line): - if ch == ' ': - removed += 1 - elif ch == '\t': - removed += tabsize - (removed % tabsize) - elif ch in '\r\n': - if DEBUG: print "dedent: %r: EOL -> strip up to EOL" % line - lines[i] = lines[i][j:] - break - else: - raise ValueError("unexpected non-whitespace char %r in " - "line %r while removing %d-space margin" - % (ch, line, margin)) - if DEBUG: - print "dedent: %r: %r -> removed %d/%d"\ - % (line, ch, removed, margin) - if removed == margin: - lines[i] = lines[i][j+1:] - break - elif removed > margin: - lines[i] = ' '*(removed-margin) + lines[i][j+1:] - break - return lines - -def _dedent(text, tabsize=8, skip_first_line=False): - """_dedent(text, tabsize=8, skip_first_line=False) -> dedented text - - "text" is the text to dedent. - "tabsize" is the tab width to use for indent width calculations. - "skip_first_line" is a boolean indicating if the first line should - be skipped for calculating the indent width and for dedenting. - This is sometimes useful for docstrings and similar. - - textwrap.dedent(s), but don't expand tabs to spaces - """ - lines = text.splitlines(1) - _dedentlines(lines, tabsize=tabsize, skip_first_line=skip_first_line) - return ''.join(lines) - - -def _get_indent(marker, s, tab_width=8): - """_get_indent(marker, s, tab_width=8) -> - (<indentation-of-'marker'>, <indentation-width>)""" - # Figure out how much the marker is indented. - INDENT_CHARS = tuple(' \t') - start = s.index(marker) - i = start - while i > 0: - if s[i-1] not in INDENT_CHARS: - break - i -= 1 - indent = s[i:start] - indent_width = 0 - for ch in indent: - if ch == ' ': - indent_width += 1 - elif ch == '\t': - indent_width += tab_width - (indent_width % tab_width) - return indent, indent_width - -def _get_trailing_whitespace(marker, s): - """Return the whitespace content trailing the given 'marker' in string 's', - up to and including a newline. - """ - suffix = '' - start = s.index(marker) + len(marker) - i = start - while i < len(s): - if s[i] in ' \t': - suffix += s[i] - elif s[i] in '\r\n': - suffix += s[i] - if s[i] == '\r' and i+1 < len(s) and s[i+1] == '\n': - suffix += s[i+1] - break - else: - break - i += 1 - return suffix - - - -#---- bash completion support -# Note: This is still experimental. I expect to change this -# significantly. -# -# To get Bash completion for a cmdln.Cmdln class, run the following -# bash command: -# $ complete -C 'python -m cmdln /path/to/script.py CmdlnClass' cmdname -# For example: -# $ complete -C 'python -m cmdln ~/bin/svn.py SVN' svn -# -#TODO: Simplify the above so don't have to given path to script (try to -# find it on PATH, if possible). Could also make class name -# optional if there is only one in the module (common case). - -if __name__ == "__main__" and len(sys.argv) == 6: - def _log(s): - return # no-op, comment out for debugging - from os.path import expanduser - fout = open(expanduser("~/tmp/bashcpln.log"), 'a') - fout.write(str(s) + '\n') - fout.close() - - # Recipe: module_from_path (1.0.1+) - def _module_from_path(path): - import imp, os, sys - path = os.path.expanduser(path) - dir = os.path.dirname(path) or os.curdir - name = os.path.splitext(os.path.basename(path))[0] - sys.path.insert(0, dir) - try: - iinfo = imp.find_module(name, [dir]) - return imp.load_module(name, *iinfo) - finally: - sys.path.remove(dir) - - def _get_bash_cplns(script_path, class_name, cmd_name, - token, preceding_token): - _log('--') - _log('get_cplns(%r, %r, %r, %r, %r)' - % (script_path, class_name, cmd_name, token, preceding_token)) - comp_line = os.environ["COMP_LINE"] - comp_point = int(os.environ["COMP_POINT"]) - _log("COMP_LINE: %r" % comp_line) - _log("COMP_POINT: %r" % comp_point) - - try: - script = _module_from_path(script_path) - except ImportError, ex: - _log("error importing `%s': %s" % (script_path, ex)) - return [] - shell = getattr(script, class_name)() - cmd_map = shell._get_canonical_map() - del cmd_map["EOF"] - - # Determine if completing the sub-command name. - parts = comp_line[:comp_point].split(None, 1) - _log(parts) - if len(parts) == 1 or not (' ' in parts[1] or '\t' in parts[1]): - #TODO: if parts[1].startswith('-'): handle top-level opts - _log("complete sub-command names") - matches = {} - for name, canon_name in cmd_map.items(): - if name.startswith(token): - matches[name] = canon_name - if not matches: - return [] - elif len(matches) == 1: - return matches.keys() - elif len(set(matches.values())) == 1: - return [matches.values()[0]] - else: - return matches.keys() - - # Otherwise, complete options for the given sub-command. - #TODO: refine this so it does the right thing with option args - if token.startswith('-'): - cmd_name = comp_line.split(None, 2)[1] - try: - cmd_canon_name = cmd_map[cmd_name] - except KeyError: - return [] - handler = shell._get_cmd_handler(cmd_canon_name) - optparser = getattr(handler, "optparser", None) - if optparser is None: - optparser = SubCmdOptionParser() - opt_strs = [] - for option in optparser.option_list: - for opt_str in option._short_opts + option._long_opts: - if opt_str.startswith(token): - opt_strs.append(opt_str) - return opt_strs - - return [] - - for cpln in _get_bash_cplns(*sys.argv[1:]): - print cpln - diff --git a/packaging/mic.spec b/packaging/mic.spec index 1c7ffac..3756dbf 100644 --- a/packaging/mic.spec +++ b/packaging/mic.spec @@ -1,4 +1,5 @@ %{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} +%{!?python_version: %define python_version %(%{__python} -c "import sys; sys.stdout.write(sys.version[:3])")} %define rc_version 0 @@ -22,6 +23,10 @@ Requires: python-urlgrabber >= 3.9.0 Requires: python-xml %endif +%if "%{?python_version}" < "2.7" +Requires: python-argparse +%endif + %if 0%{?tizen_version:1} Requires: python-rpm %else diff --git a/plugins/imager/fs_plugin.py b/plugins/imager/fs_plugin.py index faee633..d74530f 100644 --- a/plugins/imager/fs_plugin.py +++ b/plugins/imager/fs_plugin.py @@ -16,7 +16,7 @@ # Temple Place - Suite 330, Boston, MA 02111-1307, USA. from mic import chroot, msger, rt_util -from mic.utils import cmdln, misc, errors, fs_related +from mic.utils import misc, errors, fs_related from mic.imager import fs from mic.conf import configmgr from mic.plugin import pluginmgr @@ -26,12 +26,7 @@ class FsPlugin(ImagerPlugin): name = 'fs' @classmethod - @cmdln.option("--include-src", - dest = "include_src", - action = "store_true", - default = False, - help = "Generate a image with source rpms included") - def do_create(self, subcmd, opts, *args): + def do_create(self, args): """${cmd_name}: create fs image Usage: @@ -40,11 +35,11 @@ class FsPlugin(ImagerPlugin): ${cmd_option_list} """ - if len(args) != 1: - raise errors.Usage("Extra arguments given") + if args is None: + raise errors.Usage("Invalid arguments.") creatoropts = configmgr.create - ksconf = args[0] + ksconf = args.ksfile if creatoropts['runtime'] == 'bootstrap': configmgr._ksconf = ksconf @@ -83,7 +78,7 @@ class FsPlugin(ImagerPlugin): ','.join(backends.keys()))) creator = fs.FsImageCreator(creatoropts, pkgmgr) - creator._include_src = opts.include_src + creator._include_src = args.include_src if len(recording_pkgs) > 0: creator._recording_pkgs = recording_pkgs @@ -98,7 +93,7 @@ class FsPlugin(ImagerPlugin): creator.mount(None, creatoropts["cachedir"]) creator.install() #Download the source packages ###private options - if opts.include_src: + if args.include_src: installed_pkgs = creator.get_installed_packages() msger.info('--------------------------------------------------') msger.info('Generating the image with source rpms included ...') diff --git a/plugins/imager/loop_plugin.py b/plugins/imager/loop_plugin.py index f0f2771..1830230 100644 --- a/plugins/imager/loop_plugin.py +++ b/plugins/imager/loop_plugin.py @@ -20,7 +20,7 @@ import shutil import tempfile from mic import chroot, msger, rt_util -from mic.utils import misc, fs_related, errors, cmdln +from mic.utils import misc, fs_related, errors from mic.conf import configmgr from mic.plugin import pluginmgr from mic.imager.loop import LoopImageCreator, load_mountpoints @@ -30,16 +30,7 @@ class LoopPlugin(ImagerPlugin): name = 'loop' @classmethod - @cmdln.option("--compress-disk-image", dest="compress_image", - type='choice', choices=("gz", "bz2"), default=None, - help="Same with --compress-image") - # alias to compress-image for compatibility - @cmdln.option("--compress-image", dest="compress_image", - type='choice', choices=("gz", "bz2"), default=None, - help="Compress all loop images with 'gz' or 'bz2'") - @cmdln.option("--shrink", action='store_true', default=False, - help="Whether to shrink loop images to minimal size") - def do_create(self, subcmd, opts, *args): + def do_create(self, args): """${cmd_name}: create loop image Usage: @@ -48,16 +39,16 @@ class LoopPlugin(ImagerPlugin): ${cmd_option_list} """ - if len(args) != 1: - raise errors.Usage("Extra arguments given") + if args is None: + raise errors.Usage("Invalid arguments") creatoropts = configmgr.create - ksconf = args[0] + ksconf = args.ksfile if creatoropts['runtime'] == "bootstrap": configmgr._ksconf = ksconf rt_util.bootstrap_mic() - + recording_pkgs = [] if len(creatoropts['record_pkgs']) > 0: recording_pkgs = creatoropts['record_pkgs'] @@ -92,8 +83,8 @@ class LoopPlugin(ImagerPlugin): creator = LoopImageCreator(creatoropts, pkgmgr, - opts.compress_image, - opts.shrink) + args.compress_image, + args.shrink) if len(recording_pkgs) > 0: creator._recording_pkgs = recording_pkgs @@ -228,8 +219,8 @@ class LoopPlugin(ImagerPlugin): raise try: - if len(cmd) != 0: - cmdline = ' '.join(cmd) + if cmd is not None: + cmdline = cmd else: cmdline = "/bin/bash" envcmd = fs_related.find_binary_inchroot("env", extmnt) diff --git a/plugins/imager/qcow_plugin.py b/plugins/imager/qcow_plugin.py index 17b707c..8acf572 100644 --- a/plugins/imager/qcow_plugin.py +++ b/plugins/imager/qcow_plugin.py @@ -64,7 +64,7 @@ class QcowPlugin(ImagerPlugin): name = 'qcow' @classmethod - def do_create(cls, subcmd, opts, *args): + def do_create(cls, args): """${cmd_name}: create qcow image Usage: @@ -72,11 +72,11 @@ class QcowPlugin(ImagerPlugin): ${cmd_option_list} """ - if len(args) != 1: - raise errors.Usage("Extra arguments given") + if args is None: + raise errors.Usage("Invalid arguments") creatoropts = configmgr.create - ksconf = args[0] + ksconf = args.ksfile if creatoropts['runtime'] == "bootstrap": configmgr._ksconf = ksconf @@ -110,7 +110,7 @@ class QcowPlugin(ImagerPlugin): if not pkgmgr: raise errors.CreatorError("Can't find backend: %s, " - "available choices: %s" % + "available choices: %s" % (creatoropts['pkgmgr'], ','.join(backends.keys()))) @@ -30,213 +30,233 @@ import os import sys import errno -from mic import msger, creator, __version__ as VERSION -from mic.utils import cmdln, misc, errors +from argparse import ArgumentParser, SUPPRESS + +from mic import msger, __version__ as VERSION +from mic.utils import misc, errors from mic.conf import configmgr from mic.plugin import pluginmgr +from mic.helpformat import MICHelpFormatter, subparser + +@subparser +def chroot_parser(parser): + """chroot into an image -def optparser_setup(func): - """Setup optparser for a function""" - if not hasattr(func, "optparser"): - func.optparser = cmdln.SubCmdOptionParser() - func.optparser.disable_interspersed_args() - return func - - -class MicCmd(cmdln.Cmdln): + Examples: + mic chroot platform.img + mic chroot platform.img ls """ - Usage: mic SUBCOMMAND [OPTS] [ARGS...] - - mic Means the Image Creation tool - Try 'mic help SUBCOMMAND' for help on a specific subcommand. - - ${command_list} - global ${option_list} - ${help_list} + parser.add_argument('imagefile', help='Path of image file') + parser.add_argument('-s', '--saveto', action = 'store', dest = 'saveto', default = None, + help = "Save the unpacked image to specified dir") + parser.add_argument('-c', '--cmd', dest = 'cmd', default = None, + help = "command which will be executed in chroot environment") + parser.set_defaults(alias="ch") + return parser + +@subparser +def create_parser(parser): + """create an image + Examples: + $ mic -d -v create auto handset_blackbay.ks + $ mic -d -v cr loop handset_blackbay.ks --logfile=mic.log """ - name = 'mic' - version = VERSION - def print_version(self): + parent_parser = ArgumentParser(add_help=False) + parent_parser.add_argument('ksfile', help='Path of ksfile'); + parent_parser.add_argument('--logfile', dest='logfile', default=None, + help='Path of logfile') + parent_parser.add_argument('-c', '--config', dest='config', default=None, + help='Specify config file for mic') + parent_parser.add_argument('-k', '--cachedir', action='store', + dest='cachedir', default=None, + help='Cache directory to store the downloaded') + parent_parser.add_argument('-o', '--outdir', action='store', dest='outdir', + default=None, help='Output directory') + parent_parser.add_argument('-A', '--arch', dest='arch', default=None, + help='Specify repo architecture') + parent_parser.add_argument('--release', dest='release', default=None, metavar='RID', + help='Generate a release of RID with all necessary' + ' files, when @BUILD_ID@ is contained in ' + 'kickstart file, it will be replaced by RID') + parent_parser.add_argument("--record-pkgs", dest="record_pkgs", default=None, + help='Record the info of installed packages, ' + 'multiple values can be specified which ' + 'joined by ",", valid values: "name", ' + '"content", "license", "vcs"') + parent_parser.add_argument('--pkgmgr', dest='pkgmgr', default=None, + help='Specify backend package manager') + parent_parser.add_argument('--local-pkgs-path', dest='local_pkgs_path', default=None, + help='Path for local pkgs(rpms) to be installed') + parent_parser.add_argument('--runtime', dest='runtime', default=None, + help='Specify runtime mode, avaiable: bootstrap') + # --taring-to is alias to --pack-to + parent_parser.add_argument('--taring-to', dest='pack_to', default=None, + help=SUPPRESS) + parent_parser.add_argument('--pack-to', dest='pack_to', default=None, + help='Pack the images together into the specified' + ' achive, extension supported: .zip, .tar, ' + '.tar.gz, .tar.bz2, etc. by default, .tar ' + 'will be used') + parent_parser.add_argument('--copy-kernel', action='store_true', dest='copy_kernel', + help='Copy kernel files from image /boot directory' + ' to the image output directory.') + parent_parser.add_argument('--install-pkgs', action='store', dest='install_pkgs', default=None, + help='Specify what type of packages to be installed,' + ' valid: source, debuginfo, debugsource') + parent_parser.add_argument('--check-pkgs', action='store', dest='check_pkgs', default=[], + help='Check if given packages would be installed, ' + 'packages should be separated by comma') + parent_parser.add_argument('--tmpfs', action='store_true', dest='enabletmpfs', + help='Setup tmpdir as tmpfs to accelerate, experimental' + ' feature, use it if you have more than 4G memory') + parent_parser.add_argument('--repourl', action='append', dest='repourl', default=[], + help=SUPPRESS) + parent_parser.add_argument('-R', '--repo', action='append', + dest='repo', default=[], + help=SUPPRESS) + parent_parser.add_argument('--ignore-ksrepo', action='store_true', + dest='ignore_ksrepo', default=False, + help=SUPPRESS) + parent_parser.add_argument('--strict-mode', action='store_true', + dest='strict_mode', default=False, + help='Abort creation of image, if there are some errors' + ' during rpm installation. ') + parser.set_defaults(alias="cr") + + subparsers = parser.add_subparsers(title='Subcommands', dest='subcommand') + fs_parser = subparsers.add_parser('fs', parents=[parent_parser], + help='auto detect image type from magic header') + fs_parser.add_argument("--include-src", dest = "include_src",action = "store_true", + default = False, help = "Generate a image with source rpms included") + + auto_parser = subparsers.add_parser('auto', parents=[parent_parser], help='create fs image') + loop_parser = subparsers.add_parser('loop', parents=[parent_parser], help='create loop image') + + loop_parser.add_argument("--compress-disk-image", dest="compress_image", + choices=("gz", "bz2", "lzo"), default=None, + help="Same with --compress-image") + # alias to compress-image for compatibility + loop_parser.add_argument("--compress-image", dest="compress_image", + choices=("gz", "bz2", "lzo"), default=None, + help="Compress all loop images with 'gz' or 'bz2' or 'lzo'," + "Note: if you want to use 'lzo', package 'lzop' is needed to" + "be installed manually.") + loop_parser.add_argument("--shrink", action='store_true', default=False, + help="Whether to shrink loop images to minimal size") + + qcow_parser = subparsers.add_parser('qcow', parents=[parent_parser], help='create qcow image') + + return parser + +def main(argv): + """Script entry point.""" + + def print_version(): """log name, verion, hostname""" - - msger.raw("%s %s (%s)" % (self.name, - self.version, + + name = 'mic' + msger.raw("%s %s (%s)" % (name, + VERSION, misc.get_hostname_distro_str())) - - def get_optparser(self): - optparser = cmdln.CmdlnOptionParser(self, version=self.version) - # hook optparse print_version here - optparser.print_version = self.print_version - optparser.add_option('-d', '--debug', action='store_true', - dest='debug', - help='print debug message') - optparser.add_option('-v', '--verbose', action='store_true', - dest='verbose', - help='verbose information') - optparser.add_option('-i', '--interactive', action='store_true', - dest='interactive', default='True', - help='interactive output') - optparser.add_option('--non-interactive', action='store_false', - dest='interactive', default='True', - help='non-interactive output') - return optparser - - def postoptparse(self): - if self.options.interactive: - msger.enable_interactive() - else: - msger.disable_interactive() - - if self.options.verbose: - msger.set_loglevel('VERBOSE') - - if self.options.debug: - try: - import rpm - rpm.setVerbosity(rpm.RPMLOG_NOTICE) - except ImportError: - pass - - msger.set_loglevel('DEBUG') - - self.print_version() - - def help_create(self): - """Get help info from doc string. - Fill symbols with real parameters + + def has_parameter(arg, arglist): """ - crobj = creator.Creator() - crobj.optparser = crobj.get_optparser() - doc = crobj.__doc__ - doc = crobj.help_reindent(doc) - doc = crobj.help_preprocess(doc, None) - doc = doc.replace(crobj.name, "${cmd_name}", 1) - doc = doc.rstrip() + '\n' - return doc - - @cmdln.alias("cr") - def do_create(self, argv): - """Main for creating image""" - crobj = creator.Creator() - crobj.main(argv[1:]) - - def _root_confirm(self): - """Make sure command is called by root - There are a lot of commands needed to be run during creating images, - some of them must be run with root privilege like mount, kpartx""" - if os.geteuid() != 0: - msger.error('Root permission is required to continue, abort') - - @cmdln.alias("cv") - @cmdln.option("-S", "--shell", - action="store_true", dest="shell", default=False, - help="Launch shell before packaging the converted image") - def do_convert(self, _subcmd, opts, *args): - """${cmd_name}: convert image format - - Usage: - mic convert <imagefile> <destformat> - - ${cmd_option_list} + Helper function. + Check if argument requires parameter by analyzing + its action. Parameter is required only for 'store' and 'append' actions """ - if not args or len(args) != 2: - # print help - handler = self._get_cmd_handler('convert') - if hasattr(handler, "optparser"): - handler.optparser.print_help() - raise errors.Usage("2 arguments and only 2 are required") - - (srcimg, destformat) = args - - if not os.path.exists(srcimg): - raise errors.CreatorError("Cannot find the image: %s" % srcimg) - - self._root_confirm() - - configmgr.convert['shell'] = opts.shell - - srcformat = misc.get_image_type(srcimg) - if srcformat == "ext3fsimg": - srcformat = "loop" - - srcimager = None - destimager = None - for iname, icls in pluginmgr.get_plugins('imager').iteritems(): - if iname == srcformat and hasattr(icls, "do_unpack"): - srcimager = icls - if iname == destformat and hasattr(icls, "do_pack"): - destimager = icls - - if (srcimager and destimager) is None: - raise errors.CreatorError("Can't convert from %s to %s" \ - % (srcformat, destformat)) - maptab = { - "loop": "img", - } - if destformat in maptab: - imgname = os.path.splitext(os.path.basename(srcimg))[0] - dstname = "{0}.{1}".format(imgname, maptab[destformat]) - if os.path.exists(dstname): - if msger.ask("Converted image %s seems existed, " - "remove and continue?" % dstname): - os.unlink(dstname) - else: - raise errors.Abort("Canceled") - - destimager.do_pack(srcimager.do_unpack(srcimg)) - - @cmdln.alias("ch") - @cmdln.option('-s', '--saveto', - action = 'store', dest = 'saveto', default = None, - help = "Save the unpacked image to specified dir") - @optparser_setup - def do_chroot(self, _subcmd, opts, *args): - """${cmd_name}: chroot into an image - - Usage: - mic chroot [options] <imagefile> [command [arg]...] - - ${cmd_option_list} - """ - if not args: - # print help - handler = self._get_cmd_handler('chroot') - if hasattr(handler, "optparser"): - handler.optparser.print_help() - return 1 - - targetimage = args[0] - if not os.path.exists(targetimage): - raise errors.CreatorError("Cannot find the image: %s" - % targetimage) - - self._root_confirm() + if arg.startswith('-'): + for args in arglist: + if arg in (args['short'], args['long']): + if args.get('action') in (None, 'store', 'append'): + return True + return False + + # Create top level parser + epilog = "Try 'mic SUBCOMMAND --help' for help on a specific subcommand." + description = "mic - the Image Creation tool" + parser = ArgumentParser(description=description, epilog=epilog, + formatter_class=MICHelpFormatter) + + # List of global arguments + # The main purpose of this structure is to contain arguments + # of add_argument. This is used to do aliasing properly + # (see code under the comment 'replace aliases with real commands') + global_args = [{'short': '-V', 'long': '--version', 'action': 'version', + 'version': '%(prog)s ' + VERSION}, + {'short': '-d', 'long': '--debug', 'action': 'store_true', + 'help': 'debug output'}, + {'short': '-v', 'long': '--verbose', 'action': 'store_true', + 'help': 'verbose output'}, + {'short': '-i', 'long': '--interactive', 'action': 'store_true', + 'dest': 'interactive', 'default': 'True', 'help': 'interactive output'}, + {'short': '', 'long': '--non-interactive', 'action': 'store_false', + 'dest': 'interactive', 'default': 'True', 'help': 'non-interactive output'}, + ] + + for args in global_args: + parser_kwargs = {} + for key in ('action', 'help', 'version', 'default', 'dest'): + if key in args: + parser_kwargs[key] = args[key] + + if args['short'] is '': + parser.add_argument(args['long'], **parser_kwargs) + else: + parser.add_argument(args['short'], args['long'], **parser_kwargs) + + # hacked by the request of cmdln lovers + parser.format_usage = parser.format_help + + # Create parsers for subcommands + subparsers = parser.add_subparsers(title='subcommands') + + # collect aliases + aliases = {} + for name, obj in globals().iteritems(): + if name.endswith('_parser') and callable(obj): + aliases[obj(subparsers).get_default('alias')] = name.split('_')[0] + + # replace aliases with real commands + for i, arg in enumerate(argv[1:]): + if not arg.startswith('-'): + # argv[i] is previous argument to arg + if not has_parameter(argv[i], global_args) and arg in aliases: + argv[i+1] = aliases[arg] + break + + # Parse arguments + args = parser.parse_args(argv[1:]) - configmgr.chroot['saveto'] = opts.saveto + if args.interactive: + msger.enable_interactive() + else: + msger.disable_interactive() - imagetype = misc.get_image_type(targetimage) - if imagetype in ("ext3fsimg", "ext4fsimg", "btrfsimg"): - imagetype = "loop" + if args.verbose: + msger.set_loglevel('VERBOSE') - chrootclass = None - for pname, pcls in pluginmgr.get_plugins('imager').iteritems(): - if pname == imagetype and hasattr(pcls, "do_chroot"): - chrootclass = pcls - break + if args.debug: + try: + import rpm + rpm.setVerbosity(rpm.RPMLOG_NOTICE) + except ImportError: + pass - if not chrootclass: - raise errors.CreatorError("Cannot support image type: %s" \ - % imagetype) + msger.set_loglevel('DEBUG') - chrootclass.do_chroot(targetimage, args[1:]) + print_version() + # Import target module and call 'main' from it + module = __import__("mic.%s" % args.module, fromlist=[args.module]) + return module.main(parser, args, argv[1:]) + if __name__ == "__main__": try: - MIC = MicCmd() - sys.exit(MIC.main()) + sys.exit(main(sys.argv)) except KeyboardInterrupt: msger.error('\n^C catched, program aborted.') except IOError as ioerr: |