X-Git-Url: https://git.distorted.org.uk/~mdw/stgit/blobdiff_plain/c920531e512b1776d3698fd05755f8791b3be831..06848faba60e1c4e637b15b608e5bd94989c4196:/stgit/main.py diff --git a/stgit/main.py b/stgit/main.py index d93f2eb..3c8e8f4 100644 --- a/stgit/main.py +++ b/stgit/main.py @@ -19,761 +19,143 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import sys, os -from optparse import OptionParser, make_option +from optparse import OptionParser -from stgit.utils import * -from stgit import stack, git -from stgit.version import version -from stgit.config import config - - -# Main exception class -class MainException(Exception): - pass - - -# Utility functions -def __git_id(string): - """Return the GIT id - """ - if not string: - return None - - string_list = string.split('/') - - if len(string_list) == 1: - patch_name = None - git_id = string_list[0] - - if git_id == 'HEAD': - return git.get_head() - if git_id == 'base': - return read_string(crt_series.get_base_file()) - - for path in [os.path.join(git.base_dir, 'refs', 'heads'), - os.path.join(git.base_dir, 'refs', 'tags')]: - id_file = os.path.join(path, git_id) - if os.path.isfile(id_file): - return read_string(id_file) - elif len(string_list) == 2: - patch_name = string_list[0] - if patch_name == '': - patch_name = crt_series.get_current() - git_id = string_list[1] - - if not patch_name: - raise MainException, 'No patches applied' - elif not (patch_name in crt_series.get_applied() - + crt_series.get_unapplied()): - raise MainException, 'Unknown patch "%s"' % patch_name - - if git_id == 'bottom': - return crt_series.get_patch(patch_name).get_bottom() - if git_id == 'top': - return crt_series.get_patch(patch_name).get_top() - - raise MainException, 'Unknown id: %s' % string - -def __check_local_changes(): - if git.local_changes(): - raise MainException, \ - 'local changes in the tree. Use "refresh" to commit them' - -def __check_head_top_equal(): - if not crt_series.head_top_equal(): - raise MainException, \ - 'HEAD and top are not the same. You probably committed\n' \ - ' changes to the tree ouside of StGIT. If you know what you\n' \ - ' are doing, use the "refresh -f" command' - -def __check_conflicts(): - if os.path.exists(os.path.join(git.base_dir, 'conflicts')): - raise MainException, 'Unsolved conflicts. Please resolve them first' - -def __print_crt_patch(): - patch = crt_series.get_current() - if patch: - print 'Now at patch "%s"' % patch - else: - print 'No patches applied' - - -# -# Command functions -# -class Command: - """This class is used to store the command details - """ - def __init__(self, func, help, usage, option_list): - self.func = func - self.help = help - self.usage = usage - self.option_list = option_list - - -def init(parser, options, args): - """Performs the repository initialisation - """ - if len(args) != 0: - parser.error('incorrect number of arguments') - - crt_series.init() - -init_cmd = \ - Command(init, - 'initialise the tree for use with StGIT', - '%prog', - []) - - -def add(parser, options, args): - """Add files or directories to the repository - """ - if len(args) < 1: - parser.error('incorrect number of arguments') - - git.add(args) - -add_cmd = \ - Command(add, - 'add files or directories to the repository', - '%prog ', - []) - - -def rm(parser, options, args): - """Remove files from the repository - """ - if len(args) < 1: - parser.error('incorrect number of arguments') - - git.rm(args, options.force) - -rm_cmd = \ - Command(rm, - 'remove files from the repository', - '%prog [options] ', - [make_option('-f', '--force', - help = 'force removing even if the file exists', - action = 'store_true')]) - - -def status(parser, options, args): - """Show the tree status - """ - git.status(args, options.modified, options.new, options.deleted, - options.conflict, options.unknown) - -status_cmd = \ - Command(status, - 'show the tree status', - '%prog [options] []', - [make_option('-m', '--modified', - help = 'show modified files only', - action = 'store_true'), - make_option('-n', '--new', - help = 'show new files only', - action = 'store_true'), - make_option('-d', '--deleted', - help = 'show deleted files only', - action = 'store_true'), - make_option('-c', '--conflict', - help = 'show conflict files only', - action = 'store_true'), - make_option('-u', '--unknown', - help = 'show unknown files only', - action = 'store_true')]) - - -def diff(parser, options, args): - """Show the tree diff - """ - if options.revs: - rev_list = options.revs.split(':') - rev_list_len = len(rev_list) - if rev_list_len == 1: - if rev_list[0][-1] == '/': - # the whole patch - rev1 = rev_list[0] + 'bottom' - rev2 = rev_list[0] + 'top' - else: - rev1 = rev_list[0] - rev2 = None - elif rev_list_len == 2: - rev1 = rev_list[0] - rev2 = rev_list[1] - if rev2 == '': - rev2 = 'HEAD' - else: - parser.error('incorrect parameters to -r') - else: - rev1 = 'HEAD' - rev2 = None - - if options.stat: - print git.diffstat(args, __git_id(rev1), __git_id(rev2)) - else: - git.diff(args, __git_id(rev1), __git_id(rev2)) - -diff_cmd = \ - Command(diff, - 'show the tree diff', - '%prog [options] []\n\n' - 'The revision format is "([patch]/[bottom | top]) | "', - [make_option('-r', metavar = 'rev1[:[rev2]]', dest = 'revs', - help = 'show the diff between revisions'), - make_option('-s', '--stat', - help = 'show the stat instead of the diff', - action = 'store_true')]) - - -def files(parser, options, args): - """Show the files modified by a patch (or the current patch) - """ - if len(args) == 0: - patch = '' - elif len(args) == 1: - patch = args[0] - else: - parser.error('incorrect number of arguments') - - rev1 = __git_id('%s/bottom' % patch) - rev2 = __git_id('%s/top' % patch) - - if options.stat: - print git.diffstat(rev1 = rev1, rev2 = rev2) - else: - print git.files(rev1, rev2) - -files_cmd = \ - Command(files, - 'show the files modified by a patch (or the current patch)', - '%prog [options] []', - [make_option('-s', '--stat', - help = 'show the diff stat', - action = 'store_true')]) - - -def refresh(parser, options, args): - if len(args) != 0: - parser.error('incorrect number of arguments') - - if config.has_option('stgit', 'autoresolved'): - autoresolved = config.get('stgit', 'autoresolved') - else: - autoresolved = 'no' - - if autoresolved != 'yes': - __check_conflicts() - - patch = crt_series.get_current() - if not patch: - raise MainException, 'No patches applied' - - if not options.force: - __check_head_top_equal() - - if git.local_changes() \ - or not crt_series.head_top_equal() \ - or options.edit or options.message \ - or options.authname or options.authemail or options.authdate \ - or options.commname or options.commemail: - print 'Refreshing patch "%s"...' % patch, - sys.stdout.flush() - - if autoresolved == 'yes': - __resolved_all() - crt_series.refresh_patch(message = options.message, - edit = options.edit, - author_name = options.authname, - author_email = options.authemail, - author_date = options.authdate, - committer_name = options.commname, - committer_email = options.commemail) - - print 'done' - else: - print 'Patch "%s" is already up to date' % patch - -refresh_cmd = \ - Command(refresh, - 'generate a new commit for the current patch', - '%prog [options]', - [make_option('-f', '--force', - help = 'force the refresh even if HEAD and '\ - 'top differ', - action = 'store_true'), - make_option('-e', '--edit', - help = 'invoke an editor for the patch '\ - 'description', - action = 'store_true'), - make_option('-m', '--message', - help = 'use MESSAGE as the patch ' \ - 'description'), - make_option('--authname', - help = 'use AUTHNAME as the author name'), - make_option('--authemail', - help = 'use AUTHEMAIL as the author e-mail'), - make_option('--authdate', - help = 'use AUTHDATE as the author date'), - make_option('--commname', - help = 'use COMMNAME as the committer name'), - make_option('--commemail', - help = 'use COMMEMAIL as the committer ' \ - 'e-mail')]) - - -def new(parser, options, args): - """Creates a new patch - """ - if len(args) != 1: - parser.error('incorrect number of arguments') - - __check_local_changes() - __check_conflicts() - __check_head_top_equal() - - crt_series.new_patch(args[0], message = options.message, - author_name = options.authname, - author_email = options.authemail, - author_date = options.authdate, - committer_name = options.commname, - committer_email = options.commemail) - -new_cmd = \ - Command(new, - 'create a new patch and make it the topmost one', - '%prog [options] ', - [make_option('-m', '--message', - help = 'use MESSAGE as the patch description'), - make_option('--authname', - help = 'use AUTHNAME as the author name'), - make_option('--authemail', - help = 'use AUTHEMAIL as the author e-mail'), - make_option('--authdate', - help = 'use AUTHDATE as the author date'), - make_option('--commname', - help = 'use COMMNAME as the committer name'), - make_option('--commemail', - help = 'use COMMEMAIL as the committer e-mail')]) - -def delete(parser, options, args): - """Deletes a patch - """ - if len(args) != 1: - parser.error('incorrect number of arguments') - - __check_local_changes() - __check_conflicts() - __check_head_top_equal() - - crt_series.delete_patch(args[0]) - print 'Patch "%s" successfully deleted' % args[0] - __print_crt_patch() - -delete_cmd = \ - Command(delete, - 'remove the topmost or any unapplied patch', - '%prog ', - []) - - -def push(parser, options, args): - """Pushes the given patch or all onto the series - """ - # If --undo is passed, do the work and exit - if options.undo: - patch = crt_series.get_current() - if not patch: - raise MainException, 'No patch to undo' - - print 'Undoing the "%s" push...' % patch, - sys.stdout.flush() - __resolved_all() - crt_series.undo_push() - print 'done' - __print_crt_patch() - - return - - __check_local_changes() - __check_conflicts() - __check_head_top_equal() - - unapplied = crt_series.get_unapplied() - if not unapplied: - raise MainException, 'No more patches to push' - - if options.to: - boundaries = options.to.split(':') - if len(boundaries) == 1: - if boundaries[0] not in unapplied: - raise MainException, 'Patch "%s" not unapplied' % boundaries[0] - patches = unapplied[:unapplied.index(boundaries[0])+1] - elif len(boundaries) == 2: - if boundaries[0] not in unapplied: - raise MainException, 'Patch "%s" not unapplied' % boundaries[0] - if boundaries[1] not in unapplied: - raise MainException, 'Patch "%s" not unapplied' % boundaries[1] - lb = unapplied.index(boundaries[0]) - hb = unapplied.index(boundaries[1]) - if lb > hb: - raise MainException, 'Patch "%s" after "%s"' \ - % (boundaries[0], boundaries[1]) - patches = unapplied[lb:hb+1] - else: - raise MainException, 'incorrect parameters to "--to"' - elif options.number: - patches = unapplied[:options.number] - elif options.all: - patches = unapplied - elif len(args) == 0: - patches = [unapplied[0]] - elif len(args) == 1: - patches = [args[0]] - else: - parser.error('incorrect number of arguments') - - if patches == []: - raise MainException, 'No patches to push' - - if options.reverse: - patches.reverse() - - for p in patches: - print 'Pushing patch "%s"...' % p, - sys.stdout.flush() - - crt_series.push_patch(p) - - if crt_series.empty_patch(p): - print 'done (empty patch)' - else: - print 'done' - __print_crt_patch() - -push_cmd = \ - Command(push, - 'push a patch on top of the series', - '%prog [options] []', - [make_option('-a', '--all', - help = 'push all the unapplied patches', - action = 'store_true'), - make_option('-n', '--number', type = 'int', - help = 'push the specified number of patches'), - make_option('-t', '--to', metavar = 'PATCH1[:PATCH2]', - help = 'push all patches to PATCH1 or between ' - 'PATCH1 and PATCH2'), - make_option('--reverse', - help = 'push the patches in reverse order', - action = 'store_true'), - make_option('--undo', - help = 'undo the last push operation', - action = 'store_true')]) - - -def pop(parser, options, args): - if len(args) != 0: - parser.error('incorrect number of arguments') - - __check_local_changes() - __check_conflicts() - __check_head_top_equal() - - applied = crt_series.get_applied() - if not applied: - raise MainException, 'No patches applied' - applied.reverse() - - if options.to: - if options.to not in applied: - raise MainException, 'Patch "%s" not applied' % options.to - patches = applied[:applied.index(options.to)] - elif options.number: - patches = applied[:options.number] - elif options.all: - patches = applied - else: - patches = [applied[0]] - - if patches == []: - raise MainException, 'No patches to pop' - - # pop everything to the given patch - p = patches[-1] - if len(patches) == 1: - print 'Popping patch "%s"...' % p, - else: - print 'Popping "%s" - "%s" patches...' % (patches[0], p), - sys.stdout.flush() - - crt_series.pop_patch(p) - - print 'done' - __print_crt_patch() - -pop_cmd = \ - Command(pop, - 'pop the top of the series', - '%prog [options]', - [make_option('-a', '--all', - help = 'pop all the applied patches', - action = 'store_true'), - make_option('-n', '--number', type = 'int', - help = 'pop the specified number of patches'), - make_option('-t', '--to', metavar = 'PATCH', - help = 'pop all patches up to PATCH')]) - - -def __resolved(filename): - for ext in ['.local', '.older', '.remote']: - fn = filename + ext - if os.path.isfile(fn): - os.remove(fn) - -def __resolved_all(): - conflicts = git.get_conflicts() - if conflicts: - for filename in conflicts: - __resolved(filename) - os.remove(os.path.join(git.base_dir, 'conflicts')) - -def resolved(parser, options, args): - if options.all: - __resolved_all() - return - - if len(args) == 0: - parser.error('incorrect number of arguments') - - conflicts = git.get_conflicts() - if not conflicts: - raise MainException, 'No more conflicts' - # check for arguments validity - for filename in args: - if not filename in conflicts: - raise MainException, 'No conflicts for "%s"' % filename - # resolved - for filename in args: - __resolved(filename) - del conflicts[conflicts.index(filename)] - - # save or remove the conflicts file - if conflicts == []: - os.remove(os.path.join(git.base_dir, 'conflicts')) - else: - f = file(os.path.join(git.base_dir, 'conflicts'), 'w+') - f.writelines([line + '\n' for line in conflicts]) - f.close() - -resolved_cmd = \ - Command(resolved, - 'mark a file conflict as solved', - '%prog [options] [[ ]]', - [make_option('-a', '--all', - help = 'mark all conflicts as solved', - action = 'store_true')]) - - -def series(parser, options, args): - if len(args) != 0: - parser.error('incorrect number of arguments') - - applied = crt_series.get_applied() - if len(applied) > 0: - for p in applied [0:-1]: - if crt_series.empty_patch(p): - print '0', p - else: - print '+', p - p = applied[-1] - - if crt_series.empty_patch(p): - print '0>%s' % p - else: - print '> %s' % p - - for p in crt_series.get_unapplied(): - if crt_series.empty_patch(p): - print '0', p - else: - print '-', p - -series_cmd = \ - Command(series, - 'print the patch series', - '%prog', - []) - - -def applied(parser, options, args): - if len(args) != 0: - parser.error('incorrect number of arguments') - - for p in crt_series.get_applied(): - print p - -applied_cmd = \ - Command(applied, - 'print the applied patches', - '%prog', - []) - - -def unapplied(parser, options, args): - if len(args) != 0: - parser.error('incorrect number of arguments') - - for p in crt_series.get_unapplied(): - print p - -unapplied_cmd = \ - Command(unapplied, - 'print the unapplied patches', - '%prog', - []) - - -def top(parser, options, args): - if len(args) != 0: - parser.error('incorrect number of arguments') - - name = crt_series.get_current() - if name: - print name - else: - raise MainException, 'No patches applied' - -top_cmd = \ - Command(top, - 'print the name of the top patch', - '%prog', - []) - - -def export(parser, options, args): - if len(args) == 0: - dirname = 'patches' - elif len(args) == 1: - dirname = args[0] - else: - parser.error('incorrect number of arguments') - - if git.local_changes(): - print 'Warning: local changes in the tree. ' \ - 'You might want to commit them first' - - if not os.path.isdir(dirname): - os.makedirs(dirname) - series = file(os.path.join(dirname, 'series'), 'w+') - - patches = crt_series.get_applied() - num = len(patches) - zpadding = len(str(num)) - if zpadding < 2: - zpadding = 2 - - patch_no = 1; - for p in patches: - pname = p - if options.diff: - pname = '%s.diff' % pname - if options.numbered: - pname = '%s-%s' % (str(patch_no).zfill(zpadding), pname) - pfile = os.path.join(dirname, pname) - print >> series, pname - - # get the template - if options.template: - patch_tmpl = options.template - else: - patch_tmpl = os.path.join(git.base_dir, 'patchexport.tmpl') - if os.path.isfile(patch_tmpl): - tmpl = file(patch_tmpl).read() - else: - tmpl = '' - - # get the patch description - patch = crt_series.get_patch(p) - - tmpl_dict = {'description': patch.get_description().rstrip(), - 'diffstat': git.diffstat(rev1 = __git_id('%s/bottom' % p), - rev2 = __git_id('%s/top' % p)), - 'authname': patch.get_authname(), - 'authemail': patch.get_authemail(), - 'authdate': patch.get_authdate(), - 'commname': patch.get_commname(), - 'commemail': patch.get_commemail()} - for key in tmpl_dict: - if not tmpl_dict[key]: - tmpl_dict[key] = '' - - try: - descr = tmpl % tmpl_dict - except KeyError, err: - raise MainException, 'Unknown patch template variable: %s' \ - % err - except TypeError: - raise MainException, 'Only "%(name)s" variables are ' \ - 'supported in the patch template' - f = open(pfile, 'w+') - f.write(descr) - f.close() - - # write the diff - git.diff(rev1 = __git_id('%s/bottom' % p), - rev2 = __git_id('%s/top' % p), - output = pfile, append = True) - patch_no += 1 - - series.close() - -export_cmd = \ - Command(export, - 'exports a series of patches to (or patches)', - '%prog [options] []', - [make_option('-n', '--numbered', - help = 'number the patch names', - action = 'store_true'), - make_option('-d', '--diff', - help = 'append .diff to the patch names', - action = 'store_true'), - make_option('-t', '--template', metavar = 'FILE', - help = 'Use FILE as a template')]) +import stgit.commands # # The commands map # -commands = { - 'init': init_cmd, - 'add': add_cmd, - 'rm': rm_cmd, - 'status': status_cmd, - 'diff': diff_cmd, - 'files': files_cmd, - 'new': new_cmd, - 'delete': delete_cmd, - 'push': push_cmd, - 'pop': pop_cmd, - 'resolved': resolved_cmd, - 'series': series_cmd, - 'applied': applied_cmd, - 'unapplied':unapplied_cmd, - 'top': top_cmd, - 'refresh': refresh_cmd, - 'export': export_cmd, - } - +class Commands(dict): + """Commands class. It performs on-demand module loading + """ + def __getitem__(self, key): + cmd_mod = self.get(key) + __import__('stgit.commands.' + cmd_mod) + return getattr(stgit.commands, cmd_mod) + +commands = Commands({ + 'add': 'add', + 'applied': 'applied', + 'assimilate': 'assimilate', + 'branch': 'branch', + 'delete': 'delete', + 'diff': 'diff', + 'clean': 'clean', + 'clone': 'clone', + 'commit': 'commit', + 'export': 'export', + 'files': 'files', + 'float': 'float', + 'fold': 'fold', + 'goto': 'goto', + 'id': 'id', + 'import': 'imprt', + 'init': 'init', + 'log': 'log', + 'mail': 'mail', + 'new': 'new', + 'patches': 'patches', + 'pick': 'pick', + 'pop': 'pop', + 'pull': 'pull', + 'push': 'push', + 'refresh': 'refresh', + 'rename': 'rename', + 'resolved': 'resolved', + 'rm': 'rm', + 'series': 'series', + 'show': 'show', + 'status': 'status', + 'sync': 'sync', + 'top': 'top', + 'unapplied': 'unapplied', + 'uncommit': 'uncommit' + }) + +# classification: repository, stack, patch, working copy +repocommands = ( + 'branch', + 'clone', + 'id', + 'pull' + ) +stackcommands = ( + 'applied', + 'assimilate', + 'clean', + 'commit', + 'float', + 'goto', + 'init', + 'pop', + 'push', + 'series', + 'top', + 'unapplied', + 'uncommit' + ) +patchcommands = ( + 'delete', + 'export', + 'files', + 'fold', + 'import', + 'log', + 'mail', + 'new', + 'pick', + 'refresh', + 'rename', + 'show', + 'sync' + ) +wccommands = ( + 'add', + 'diff', + 'patches', + 'resolved', + 'rm', + 'status' + ) + +def _print_helpstring(cmd): + print ' ' + cmd + ' ' * (12 - len(cmd)) + commands[cmd].help + def print_help(): print 'usage: %s [options]' % os.path.basename(sys.argv[0]) print - print 'commands:' - print ' help print this message' - + print 'Generic commands:' + print ' help print the detailed command usage' + print ' version display version information' + print ' copyright display copyright information' + # unclassified commands if any cmds = commands.keys() cmds.sort() for cmd in cmds: - print ' ' + cmd + ' ' * (12 - len(cmd)) + commands[cmd].help + if not cmd in repocommands and not cmd in stackcommands \ + and not cmd in patchcommands and not cmd in wccommands: + _print_helpstring(cmd) + print + + print 'Repository commands:' + for cmd in repocommands: + _print_helpstring(cmd) + print + + print 'Stack commands:' + for cmd in stackcommands: + _print_helpstring(cmd) + print + + print 'Patch commands:' + for cmd in patchcommands: + _print_helpstring(cmd) + print + + print 'Working-copy commands:' + for cmd in wccommands: + _print_helpstring(cmd) # # The main function (command dispatcher) @@ -781,28 +163,53 @@ def print_help(): def main(): """The main function """ - global crt_series - prog = os.path.basename(sys.argv[0]) if len(sys.argv) < 2: - print >> sys.stderr, 'Unknown command' + print >> sys.stderr, 'usage: %s ' % prog print >> sys.stderr, \ - ' Try "%s help" for a list of supported commands' % prog + ' Try "%s --help" for a list of supported commands' % prog sys.exit(1) cmd = sys.argv[1] - if cmd in ['-h', '--help', 'help']: - print_help() + if cmd in ['-h', '--help']: + if len(sys.argv) >= 3 and sys.argv[2] in commands: + cmd = sys.argv[2] + sys.argv[2] = '--help' + else: + print_help() + sys.exit(0) + if cmd == 'help': + if len(sys.argv) == 3 and not sys.argv[2] in ['-h', '--help']: + cmd = sys.argv[2] + if not cmd in commands: + print >> sys.stderr, '%s help: "%s" command unknown' \ + % (prog, cmd) + sys.exit(1) + + sys.argv[0] += ' %s' % cmd + command = commands[cmd] + parser = OptionParser(usage = command.usage, + option_list = command.options) + from pydoc import pager + pager(parser.format_help()) + else: + print_help() sys.exit(0) - if cmd in ['-v', '--version']: - print '%s %s' % (prog, version) + if cmd in ['-v', '--version', 'version']: + from stgit.version import version + print 'Stacked GIT %s' % version + os.system('git --version') + print 'Python version %s' % sys.version + sys.exit(0) + if cmd in ['copyright']: + print __copyright__ sys.exit(0) if not cmd in commands: print >> sys.stderr, 'Unknown command: %s' % cmd - print >> sys.stderr, ' Try "%s help" for a list of supported commands' \ - % prog + print >> sys.stderr, ' Try "%s help" for a list of supported ' \ + 'commands' % prog sys.exit(1) # re-build the command line arguments @@ -810,15 +217,37 @@ def main(): del(sys.argv[1]) command = commands[cmd] - parser = OptionParser(usage = command.usage, - option_list = command.option_list) + usage = command.usage.split('\n')[0].strip() + parser = OptionParser(usage = usage, option_list = command.options) options, args = parser.parse_args() + + # These modules are only used from this point onwards and do not + # need to be imported earlier + from stgit.config import config_setup + from ConfigParser import ParsingError, NoSectionError + from stgit.stack import Series, StackException + from stgit.git import GitException + from stgit.commands.common import CmdException + from stgit.gitmergeonefile import GitMergeException + try: - crt_series = stack.Series() + config_setup() + + # 'clone' doesn't expect an already initialised GIT tree. A Series + # object will be created after the GIT tree is cloned + if cmd != 'clone': + if hasattr(options, 'branch') and options.branch: + command.crt_series = Series(options.branch) + else: + command.crt_series = Series() + stgit.commands.common.crt_series = command.crt_series + command.func(parser, options, args) - except (IOError, MainException, stack.StackException, git.GitException), \ - err: + except (IOError, ParsingError, NoSectionError, CmdException, + StackException, GitException, GitMergeException), err: print >> sys.stderr, '%s %s: %s' % (prog, cmd, err) sys.exit(2) + except KeyboardInterrupt: + sys.exit(1) sys.exit(0)