0001
0002
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
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
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
0136
0137
0138 takes_config_file = None
0139
0140
0141 group_name = ''
0142
0143 required_args = ()
0144 description = None
0145 usage = ''
0146 hidden = False
0147
0148
0149 default_verbosity = 0
0150
0151 default_interactive = 0
0152 return_code = 0
0153
0154 BadCommand = BadCommand
0155
0156
0157
0158
0159
0160
0161 def run(self, args):
0162 self.parse_args(args)
0163
0164
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
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
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
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
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
0387
0388
0389
0390
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 if not s:
0448 s = 'n'
0449 if s.startswith('y'):
0450 break
0451 if s.startswith('n'):
0452 return
0453 print 'Unknown response; Y or N please'
0454 else:
0455 return
0456
0457 if self.verbose:
0458 print 'Overwriting %s with new content' % filename
0459 if not self.simulate:
0460 f = open(filename, 'wb')
0461 f.write(content)
0462 f.close()
0463
0464 def insert_into_file(self, filename, marker_name, text,
0465 indent=False):
0466 """
0467 Inserts ``text`` into the file, right after the given marker.
0468 Markers look like: ``-*- <marker_name>[:]? -*-``, and the text
0469 will go on the immediately following line.
0470
0471 Raises ``ValueError`` if the marker is not found.
0472
0473 If ``indent`` is true, then the text will be indented at the
0474 same level as the marker.
0475 """
0476 if not text.endswith('\n'):
0477 raise ValueError(
0478 "The text must end with a newline: %r" % text)
0479 if not os.path.exists(filename) and self.simulate:
0480
0481
0482 if self.verbose:
0483 print 'Would (if not simulating) insert text into %s' % (
0484 self.shorten(filename))
0485 return
0486
0487 f = open(filename)
0488 lines = f.readlines()
0489 f.close()
0490 regex = re.compile(r'-\*-\s+%s:?\s+-\*-' % re.escape(marker_name),
0491 re.I)
0492 for i in range(len(lines)):
0493 if regex.search(lines[i]):
0494
0495 if (lines[i:] and len(lines[i:]) > 1 and
0496 ''.join(lines[i+1:]).strip().startswith(text.strip())):
0497
0498 print 'Warning: line already found in %s (not inserting' % filename
0499 print ' %s' % lines[i]
0500 return
0501
0502 if indent:
0503 text = text.lstrip()
0504 match = re.search(r'^[ \t]*', lines[i])
0505 text = match.group(0) + text
0506 lines[i+1:i+1] = [text]
0507 break
0508 else:
0509 errstr = (
0510 "Marker '-*- %s -*-' not found in %s"
0511 % (marker_name, filename))
0512 if 1 or self.simulate:
0513 print 'Warning: %s' % errstr
0514 else:
0515 raise ValueError(errstr)
0516 if self.verbose:
0517 print 'Updating %s' % self.shorten(filename)
0518 if not self.simulate:
0519 f = open(filename, 'w')
0520 f.write(''.join(lines))
0521 f.close()
0522
0523 def run_command(self, cmd, *args, **kw):
0524 """
0525 Runs the command, respecting verbosity and simulation.
0526 Returns stdout, or None if simulating.
0527
0528 Keyword arguments:
0529
0530 cwd:
0531 the current working directory to run the command in
0532 capture_stderr:
0533 if true, then both stdout and stderr will be returned
0534 expect_returncode:
0535 if true, then don't fail if the return code is not 0
0536 force_no_simulate:
0537 if true, run the command even if --simulate
0538 """
0539 cmd = self.quote_first_command_arg(cmd)
0540 cwd = popdefault(kw, 'cwd', os.getcwd())
0541 capture_stderr = popdefault(kw, 'capture_stderr', False)
0542 expect_returncode = popdefault(kw, 'expect_returncode', False)
0543 force = popdefault(kw, 'force_no_simulate', False)
0544 warn_returncode = popdefault(kw, 'warn_returncode', False)
0545 if warn_returncode:
0546 expect_returncode = True
0547 simulate = self.simulate
0548 if force:
0549 simulate = False
0550 assert not kw, ("Arguments not expected: %s" % kw)
0551 if capture_stderr:
0552 stderr_pipe = subprocess.STDOUT
0553 else:
0554 stderr_pipe = subprocess.PIPE
0555 try:
0556 proc = subprocess.Popen([cmd] + list(args),
0557 cwd=cwd,
0558 stderr=stderr_pipe,
0559 stdout=subprocess.PIPE)
0560 except OSError, e:
0561 if e.errno != 2:
0562
0563 raise
0564 raise OSError(
0565 "The expected executable %s was not found (%s)"
0566 % (cmd, e))
0567 if self.verbose:
0568 print 'Running %s %s' % (cmd, ' '.join(args))
0569 if simulate:
0570 return None
0571 stdout, stderr = proc.communicate()
0572 if proc.returncode and not expect_returncode:
0573 if not self.verbose:
0574 print 'Running %s %s' % (cmd, ' '.join(args))
0575 print 'Error (exit code: %s)' % proc.returncode
0576 if stderr:
0577 print stderr
0578 raise OSError("Error executing command %s" % cmd)
0579 if self.verbose > 2:
0580 if stderr:
0581 print 'Command error output:'
0582 print stderr
0583 if stdout:
0584 print 'Command output:'
0585 print stdout
0586 elif proc.returncode and warn_returncode:
0587 print 'Warning: command failed (%s %s)' % (cmd, ' '.join(args))
0588 print 'Exited with code %s' % proc.returncode
0589 return stdout
0590
0591 def quote_first_command_arg(self, arg):
0592 """
0593 There's a bug in Windows when running an executable that's
0594 located inside a path with a space in it. This method handles
0595 that case, or on non-Windows systems or an executable with no
0596 spaces, it just leaves well enough alone.
0597 """
0598 if (sys.platform != 'win32'
0599 or ' ' not in arg):
0600
0601 return arg
0602 try:
0603 import win32api
0604 except ImportError:
0605 raise ValueError(
0606 "The executable %r contains a space, and in order to "
0607 "handle this issue you must have the win32api module "
0608 "installed" % arg)
0609 arg = win32api.GetShortPathName(arg)
0610 return arg
0611
0612 _svn_failed = False
0613
0614 def svn_command(self, *args, **kw):
0615 """
0616 Run an svn command, but don't raise an exception if it fails.
0617 """
0618 try:
0619 return self.run_command('svn', *args, **kw)
0620 except OSError, e:
0621 if not self._svn_failed:
0622 print 'Unable to run svn command (%s); proceeding anyway' % e
0623 self._svn_failed = True
0624
0625 def write_file(self, filename, content, source=None,
0626 binary=True, svn_add=True):
0627 """
0628 Like ``ensure_file``, but without the interactivity. Mostly
0629 deprecated. (I think I forgot it existed)
0630 """
0631 import warnings
0632 warnings.warn(
0633 "command.write_file has been replaced with "
0634 "command.ensure_file",
0635 DeprecationWarning, 2)
0636 if os.path.exists(filename):
0637 if binary:
0638 f = open(filename, 'rb')
0639 else:
0640 f = open(filename, 'r')
0641 old_content = f.read()
0642 f.close()
0643 if content == old_content:
0644 if self.verbose:
0645 print 'File %s exists with same content' % (
0646 self.shorten(filename))
0647 return
0648 if (not self.simulate and self.options.interactive):
0649 if not self.ask('Overwrite file %s?' % filename):
0650 return
0651 if self.verbose > 1 and source:
0652 print 'Writing %s from %s' % (self.shorten(filename),
0653 self.shorten(source))
0654 elif self.verbose:
0655 print 'Writing %s' % self.shorten(filename)
0656 if not self.simulate:
0657 already_existed = os.path.exists(filename)
0658 if binary:
0659 f = open(filename, 'wb')
0660 else:
0661 f = open(filename, 'w')
0662 f.write(content)
0663 f.close()
0664 if (not already_existed
0665 and svn_add
0666 and os.path.exists(os.path.join(os.path.dirname(filename), '.svn'))):
0667 self.svn_command('add', filename)
0668
0669 def parse_vars(self, args):
0670 """
0671 Given variables like ``['a=b', 'c=d']`` turns it into ``{'a':
0672 'b', 'c': 'd'}``
0673 """
0674 result = {}
0675 for arg in args:
0676 if '=' not in arg:
0677 raise BadCommand(
0678 'Variable assignment %r invalid (no "=")'
0679 % arg)
0680 name, value = arg.split('=', 1)
0681 result[name] = value
0682 return result
0683
0684 def read_vars(self, config, section='pastescript'):
0685 """
0686 Given a configuration filename, this will return a map of values.
0687 """
0688 result = {}
0689 p = ConfigParser.RawConfigParser()
0690 p.read([config])
0691 if p.has_section(section):
0692 for key, value in p.items(section):
0693 if key.endswith('__eval__'):
0694 result[key[:-len('__eval__')]] = eval(value)
0695 else:
0696 result[key] = value
0697 return result
0698
0699 def write_vars(self, config, vars, section='pastescript'):
0700 """
0701 Given a configuration filename, this will add items in the
0702 vars mapping to the configuration file. Will create the
0703 configuration file if it doesn't exist.
0704 """
0705 modified = False
0706
0707 p = ConfigParser.RawConfigParser()
0708 if not os.path.exists(config):
0709 f = open(config, 'w')
0710 f.write('')
0711 f.close()
0712 modified = True
0713 p.read([config])
0714 if not p.has_section(section):
0715 p.add_section(section)
0716 modified = True
0717
0718 existing_options = p.options(section)
0719 for key, value in vars.items():
0720 if (key not in existing_options and
0721 '%s__eval__' % key not in existing_options):
0722 if not isinstance(value, str):
0723 p.set(section, '%s__eval__' % key, repr(value))
0724 else:
0725 p.set(section, key, value)
0726 modified = True
0727
0728 if modified:
0729 p.write(open(config, 'w'))
0730
0731 def indent_block(self, text, indent=2, initial=None):
0732 """
0733 Indent the block of text (each line is indented). If you give
0734 ``initial``, then that is used in lieue of ``indent`` for the
0735 first line.
0736 """
0737 if initial is None:
0738 initial = indent
0739 lines = text.splitlines()
0740 first = (' '*initial) + lines[0]
0741 rest = [(' '*indent)+l for l in lines[1:]]
0742 return '\n'.join([first]+rest)
0743
0744 def logging_file_config(self, config_file):
0745 """
0746 Setup logging via the logging module's fileConfig function with the
0747 specified ``config_file``, if applicable.
0748 """
0749 parser = ConfigParser.ConfigParser()
0750 parser.read([config_file])
0751 if parser.has_section('loggers'):
0752 fileConfig(config_file)
0753
0754class NotFoundCommand(Command):
0755
0756 def run(self, args):
0757
0758
0759
0760 print 'Command %s not known' % self.command_name
0761 commands = get_commands().items()
0762 commands.sort()
0763 if not commands:
0764 print 'No commands registered.'
0765 print 'Have you installed Paste Script?'
0766 print '(try running python setup.py develop)'
0767 return 2
0768 print 'Known commands:'
0769 longest = max([len(n) for n, c in commands])
0770 for name, command in commands:
0771 print ' %s %s' % (self.pad(name, length=longest),
0772 command.load().summary)
0773 return 2
0774
0775def popdefault(dict, name, default=None):
0776 if name not in dict:
0777 return default
0778 else:
0779 v = dict[name]
0780 del dict[name]
0781 return v