Files
pipenv/pipenv/cmdparse.py
T
Dan Ryan dcfce70817 Fix python path discovery if not called python
- Begin a refactor of `delegator.run` invocation to ensure we capture
  and handle failures with our own exception wrappers
- Additoinally capture output and error logging and command information
  when running in verbose mode (should avoid significant repitition in
  the codebase)
- Refactor `which` and `system_which` to fallback to pythonfinder's
implementation
- Abstract `is_python_command` to identify whether we are looking for
  python, this enables us to rely on `pythonfinder.Finder.find_all_python_versions()`
  to ensure we aren't skipping python versions
- Fixes #2783

Signed-off-by: Dan Ryan <dan@danryan.co>
2019-03-16 15:10:40 -04:00

99 lines
3.0 KiB
Python

import itertools
import re
import shlex
import six
class ScriptEmptyError(ValueError):
pass
def _quote_if_contains(value, pattern):
if next(iter(re.finditer(pattern, value)), None):
return '"{0}"'.format(re.sub(r'(\\*)"', r'\1\1\\"', value))
return value
class Script(object):
"""Parse a script line (in Pipfile's [scripts] section).
This always works in POSIX mode, even on Windows.
"""
def __init__(self, command, args=None):
self._parts = [command]
if args:
self._parts.extend(args)
@classmethod
def parse(cls, value):
if isinstance(value, six.string_types):
value = shlex.split(value)
if not value:
raise ScriptEmptyError(value)
return cls(value[0], value[1:])
def __repr__(self):
return "Script({0!r})".format(self._parts)
@property
def command(self):
return self._parts[0]
@property
def args(self):
return self._parts[1:]
def extend(self, extra_args):
self._parts.extend(extra_args)
def cmdify(self):
"""Encode into a cmd-executable string.
This re-implements CreateProcess's quoting logic to turn a list of
arguments into one single string for the shell to interpret.
* All double quotes are escaped with a backslash.
* Existing backslashes before a quote are doubled, so they are all
escaped properly.
* Backslashes elsewhere are left as-is; cmd will interpret them
literally.
The result is then quoted into a pair of double quotes to be grouped.
An argument is intentionally not quoted if it does not contain
foul characters. This is done to be compatible with Windows built-in
commands that don't work well with quotes, e.g. everything with `echo`,
and DOS-style (forward slash) switches.
Foul characters include:
* Whitespaces.
* Carets (^). (pypa/pipenv#3307)
* Parentheses in the command. (pypa/pipenv#3168)
Carets introduce a difficult situation since they are essentially
"lossy" when parsed. Consider this in cmd.exe::
> echo "foo^bar"
"foo^bar"
> echo foo^^bar
foo^bar
The two commands produce different results, but are both parsed by the
shell as `foo^bar`, and there's essentially no sensible way to tell
what was actually passed in. This implementation assumes the quoted
variation (the first) since it is easier to implement, and arguably
the more common case.
The intended use of this function is to pre-process an argument list
before passing it into ``subprocess.Popen(..., shell=True)``.
See also: https://docs.python.org/3/library/subprocess.html#converting-argument-sequence
"""
return " ".join(itertools.chain(
[_quote_if_contains(self.command, r'[\s^()]')],
(_quote_if_contains(arg, r'[\s^]') for arg in self.args),
))