0001# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
0002# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
0003import pkg_resources
0004import sys
0005import optparse
0006import bool_optparse
0007import os
0008import re
0009import textwrap
0010import pluginlib
0011import ConfigParser
0012import getpass
0013try:
0014    import subprocess
0015except ImportError:
0016    from paste.script.util import subprocess24 as subprocess
0017difflib = None
0018
0019from paste.script.util.logging_config import fileConfig
0020
0021class BadCommand(Exception):
0022
0023    def __init__(self, message, exit_code=2):
0024        self.message = message
0025        self.exit_code = exit_code
0026        Exception.__init__(self, message)
0027
0028class NoDefault(object):
0029    pass
0030
0031dist = pkg_resources.get_distribution('PasteScript')
0032
0033python_version = sys.version.splitlines()[0].strip()
0034
0035parser = optparse.OptionParser(add_help_option=False,
0036                               version='%s from %s (python %s)'
0037                               % (dist, dist.location, python_version),
0038                               usage='%prog [paster_options] COMMAND [command_options]')
0039
0040parser.add_option(
0041    '--plugin',
0042    action='append',
0043    dest='plugins',
0044    help="Add a plugin to the list of commands (plugins are Egg specs; will also require() the Egg)")
0045parser.add_option(
0046    '-h', '--help',
0047    action='store_true',
0048    dest='do_help',
0049    help="Show this help message")
0050parser.disable_interspersed_args()
0051
0052# @@: Add an option to run this in another Python interpreter
0053
0054system_plugins = []
0055
0056def run(args=None):
0057    if (not args and
0058        len(sys.argv) >= 2
0059        and os.environ.get('_') and sys.argv[0] != os.environ['_']
0060        and os.environ['_'] == sys.argv[1]):
0061        # probably it's an exe execution
0062        args = ['exe', os.environ['_']] + sys.argv[2:]
0063    if args is None:
0064        args = sys.argv[1:]
0065    options, args = parser.parse_args(args)
0066    options.base_parser = parser
0067    system_plugins.extend(options.plugins or [])
0068    commands = get_commands()
0069    if options.do_help:
0070        args = ['help'] + args
0071    if not args:
0072        print 'Usage: %s COMMAND' % sys.argv[0]
0073        args = ['help']
0074    command_name = args[0]
0075    if command_name not in commands:
0076        command = NotFoundCommand
0077    else:
0078        command = commands[command_name].load()
0079    invoke(command, command_name, options, args[1:])
0080
0081def parse_exe_file(config):
0082    import shlex
0083    p = ConfigParser.RawConfigParser()
0084    p.read([config])
0085    command_name = 'exe'
0086    options = []
0087    if p.has_option('exe', 'command'):
0088        command_name = p.get('exe', 'command')
0089    if p.has_option('exe', 'options'):
0090        options = shlex.split(p.get('exe', 'options'))
0091    if p.has_option('exe', 'sys.path'):
0092        paths = shlex.split(p.get('exe', 'sys.path'))
0093        paths = [os.path.abspath(os.path.join(os.path.dirname(config), p))
0094                 for p in paths]
0095        for path in paths:
0096            pkg_resources.working_set.add_entry(path)
0097            sys.path.insert(0, path)
0098    args = [command_name, config] + options
0099    return args
0100
0101def get_commands():
0102    plugins = system_plugins[:]
0103    egg_info_dir = pluginlib.find_egg_info_dir(os.getcwd())
0104    if egg_info_dir:
0105        plugins.append(os.path.splitext(os.path.basename(egg_info_dir))[0])
0106        base_dir = os.path.dirname(egg_info_dir)
0107        if base_dir not in sys.path:
0108            sys.path.insert(0, base_dir)
0109            pkg_resources.working_set.add_entry(base_dir)
0110    plugins = pluginlib.resolve_plugins(plugins)
0111    commands = pluginlib.load_commands_from_plugins(plugins)
0112    commands.update(pluginlib.load_global_commands())
0113    return commands
0114
0115def invoke(command, command_name, options, args):
0116    try:
0117        runner = command(command_name)
0118        exit_code = runner.run(args)
0119    except BadCommand, e:
0120        print e.message
0121        exit_code = e.exit_code
0122    sys.exit(exit_code)
0123
0124
0125class Command(object):
0126
0127    def __init__(self, name):
0128        self.command_name = name
0129
0130    max_args = None
0131    max_args_error = 'You must provide no more than %(max_args)s arguments'
0132    min_args = None
0133    min_args_error = 'You must provide at least %(min_args)s arguments'
0134    required_args = None
0135    # If this command takes a configuration file, set this to 1 or -1
0136    # Then if invoked through #! the config file will be put into the positional
0137    # arguments -- at the beginning with 1, at the end with -1
0138    takes_config_file = None
0139
0140    # Grouped in help messages by this:
0141    group_name = ''
0142
0143    required_args = ()
0144    description = None
0145    usage = ''
0146    hidden = False
0147    # This is the default verbosity level; --quiet subtracts,
0148    # --verbose adds:
0149    default_verbosity = 0
0150    # This is the default interactive state:
0151    default_interactive = 0
0152    return_code = 0
0153
0154    BadCommand = BadCommand
0155
0156    # Must define:
0157    #   parser
0158    #   summary
0159    #   command()
0160
0161    def run(self, args):
0162        self.parse_args(args)
0163
0164        # Setup defaults:
0165        for name, default in [('verbose', 0),
0166                              ('quiet', 0),
0167                              ('interactive', False),
0168                              ('overwrite', False)]:
0169            if not hasattr(self.options, name):
0170                setattr(self.options, name, default)
0171        if getattr(self.options, 'simulate', False):
0172            self.options.verbose = max(self.options.verbose, 1)
0173        self.interactive = self.default_interactive
0174        if getattr(self.options, 'interactive', False):
0175            self.interactive += self.options.interactive
0176        if getattr(self.options, 'no_interactive', False):
0177            self.interactive = False
0178        self.verbose = self.default_verbosity
0179        self.verbose += self.options.verbose
0180        self.verbose -= self.options.quiet
0181        self.simulate = getattr(self.options, 'simulate', False)
0182
0183        # For #! situations:
0184        if (os.environ.get('PASTE_CONFIG_FILE')
0185            and self.takes_config_file is not None):
0186            take = self.takes_config_file
0187            filename = os.environ.get('PASTE_CONFIG_FILE')
0188            if take == 1:
0189                self.args.insert(0, filename)
0190            elif take == -1:
0191                self.args.append(filename)
0192            else:
0193                assert 0, (
0194                    "Value takes_config_file must be None, 1, or -1 (not %r)"
0195                    % take)
0196
0197        if (os.environ.get('PASTE_DEFAULT_QUIET')):
0198            self.verbose = 0
0199
0200        # Validate:
0201        if self.min_args is not None and len(self.args) < self.min_args:
0202            raise BadCommand(
0203                self.min_args_error % {'min_args': self.min_args,
0204                                       'actual_args': len(self.args)})
0205        if self.max_args is not None and len(self.args) > self.max_args:
0206            raise BadCommand(
0207                self.max_args_error % {'max_args': self.max_args,
0208                                       'actual_args': len(self.args)})
0209        for var_name, option_name in self.required_args:
0210            if not getattr(self.options, var_name, None):
0211                raise BadCommand(
0212                    'You must provide the option %s' % option_name)
0213        result = self.command()
0214        if result is None:
0215            return self.return_code
0216        else:
0217            return result
0218
0219    def parse_args(self, args):
0220        if self.usage:
0221            usage = ' '+self.usage
0222        else:
0223            usage = ''
0224        self.parser.usage = "%%prog [options]%s\n%s" % (
0225            usage, self.summary)
0226        self.parser.prog = '%s %s' % (sys.argv[0], self.command_name)
0227        if self.description:
0228            desc = self.description
0229            desc = textwrap.dedent(desc)
0230            self.parser.description = desc
0231        self.options, self.args = self.parser.parse_args(args)
0232
0233    ########################################
0234    ## Utility methods
0235    ########################################
0236
0237    def here(cls):
0238        mod = sys.modules[cls.__module__]
0239        return os.path.dirname(mod.__file__)
0240
0241    here = classmethod(here)
0242
0243    def ask(self, prompt, safe=False, default=True):
0244        """
0245        Prompt the user.  Default can be true, false, ``'careful'`` or
0246        ``'none'``.  If ``'none'`` then the user must enter y/n.  If
0247        ``'careful'`` then the user must enter yes/no (long form).
0248
0249        If the interactive option is over two (``-ii``) then ``safe``
0250        will be used as a default.  This option should be the
0251        do-nothing option.
0252        """
0253        # @@: Should careful be a separate argument?
0254
0255        if self.options.interactive >= 2:
0256            default = safe
0257        if default == 'careful':
0258            prompt += ' [yes/no]?'
0259        elif default == 'none':
0260            prompt += ' [y/n]?'
0261        elif default:
0262            prompt += ' [Y/n]? '
0263        else:
0264            prompt += ' [y/N]? '
0265        while 1:
0266            response = raw_input(prompt).strip().lower()
0267            if not response:
0268                if default in ('careful', 'none'):
0269                    print 'Please enter yes or no'
0270                    continue
0271                return default
0272            if default == 'careful':
0273                if response in ('yes', 'no'):
0274                    return response == 'yes'
0275                print 'Please enter "yes" or "no"'
0276                continue
0277            if response[0].lower() in ('y', 'n'):
0278                return response[0].lower() == 'y'
0279            print 'Y or N please'
0280
0281    def challenge(self, prompt, default=NoDefault, should_echo=True):
0282        """
0283        Prompt the user for a variable.
0284        """
0285        if default is not NoDefault:
0286            prompt += ' [%r]' % default
0287        prompt += ': '
0288        while 1:
0289            if should_echo:
0290                prompt_method = raw_input
0291            else:
0292                prompt_method = getpass.getpass
0293            response = prompt_method(prompt).strip()
0294            if not response:
0295                if default is not NoDefault:
0296                    return default
0297                else:
0298                    continue
0299            else:
0300                return response
0301
0302    def pad(self, s, length, dir='left'):
0303        if len(s) >= length:
0304            return s
0305        if dir == 'left':
0306            return s + ' '*(length-len(s))
0307        else:
0308            return ' '*(length-len(s)) + s
0309
0310    def standard_parser(cls, verbose=True,
0311                        interactive=False,
0312                        no_interactive=False,
0313                        simulate=False,
0314                        quiet=False,
0315                        overwrite=False):
0316        """
0317        Create a standard ``OptionParser`` instance.
0318        
0319        Typically used like::
0320
0321            class MyCommand(Command):
0322                parser = Command.standard_parser()
0323
0324        Subclasses may redefine ``standard_parser``, so use the
0325        nearest superclass's class method.
0326        """
0327        parser = bool_optparse.BoolOptionParser()
0328        if verbose:
0329            parser.add_option('-v', '--verbose',
0330                              action='count',
0331                              dest='verbose',
0332                              default=0)
0333        if quiet:
0334            parser.add_option('-q', '--quiet',
0335                              action='count',
0336                              dest='quiet',
0337                              default=0)
0338        if no_interactive:
0339            parser.add_option('--no-interactive',
0340                              action="count",
0341                              dest="no_interactive",
0342                              default=0)
0343        if interactive:
0344            parser.add_option('-i', '--interactive',
0345                              action='count',
0346                              dest='interactive',
0347                              default=0)
0348        if simulate:
0349            parser.add_option('-n', '--simulate',
0350                              action='store_true',
0351                              dest='simulate',
0352                              default=False)
0353        if overwrite:
0354            parser.add_option('-f', '--overwrite',
0355                              dest="overwrite",
0356                              action="store_true",
0357                              help="Overwrite files (warnings will be emitted for non-matching files otherwise)")
0358        return parser
0359
0360    standard_parser = classmethod(standard_parser)
0361
0362    def shorten(self, fn, *paths):
0363        """
0364        Return a shorted form of the filename (relative to the current
0365        directory), typically for displaying in messages.  If
0366        ``*paths`` are present, then use os.path.join to create the
0367        full filename before shortening.
0368        """
0369        if paths:
0370            fn = os.path.join(fn, *paths)
0371        if fn.startswith(os.getcwd()):
0372            return fn[len(os.getcwd()):].lstrip(os.path.sep)
0373        else:
0374            return fn
0375
0376    def ensure_dir(self, dir, svn_add=True):
0377        """
0378        Ensure that the directory exists, creating it if necessary.
0379        Respects verbosity and simulation.
0380
0381        Adds directory to subversion if ``.svn/`` directory exists in
0382        parent, and directory was created.
0383        """
0384        dir = dir.rstrip(os.sep)
0385        if not dir:
0386            # we either reached the parent-most directory, or we got
0387            # a relative directory
0388            # @@: Should we make sure we resolve relative directories
0389            # first?  Though presumably the current directory always
0390            # exists.
0391            return
0392        if not os.path.exists(dir):
0393            self.ensure_dir(os.path.dirname(dir))
0394            if self.verbose:
0395                print 'Creating %s' % self.shorten(dir)
0396            if not self.simulate:
0397                os.mkdir(dir)
0398            if (svn_add and
0399                os.path.exists(os.path.join(os.path.dirname(dir), '.svn'))):
0400                self.svn_command('add', dir)
0401        else:
0402            if self.verbose > 1:
0403                print "Directory already exists: %s" % self.shorten(dir)
0404
0405    def ensure_file(self, filename, content, svn_add=True):
0406        """
0407        Ensure a file named ``filename`` exists with the given
0408        content.  If ``--interactive`` has been enabled, this will ask
0409        the user what to do if a file exists with different content.
0410        """
0411        global difflib
0412        assert content is not None, (
0413            "You cannot pass a content of None")
0414        self.ensure_dir(os.path.dirname(filename), svn_add=svn_add)
0415        if not os.path.exists(filename):
0416            if self.verbose:
0417                print 'Creating %s' % filename
0418            if not self.simulate:
0419                f = open(filename, 'wb')
0420                f.write(content)
0421                f.close()
0422            if svn_add and os.path.exists(os.path.join(os.path.dirname(filename), '.svn')):
0423                self.svn_command('add', filename,
0424                                 warn_returncode=True)
0425            return
0426        f = open(filename, 'rb')
0427        old_content = f.read()
0428        f.close()
0429        if content == old_content:
0430            if self.verbose > 1:
0431                print 'File %s matches expected content' % filename
0432            return
0433        if not self.options.overwrite:
0434            print 'Warning: file %s does not match expected content' % filename
0435            if difflib is None:
0436                import difflib
0437            diff = difflib.context_diff(
0438                content.splitlines(),
0439                old_content.splitlines(),
0440                'expected ' + filename,
0441                filename)
0442            print '\n'.join(diff)
0443            if self.interactive:
0444                while 1:
0445                    s = raw_input(
0446                        'Overwrite file with new content? [y/N] ').strip().lower()
0447            �