Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issue #3901: environment variables parse in commands #4192

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0cc804b
some tests.
Apr 13, 2020
9f8ad74
Changed case of variable in cmdparse.py.
Apr 13, 2020
a9ef4c8
Removed useless comment.
Apr 13, 2020
23fc2d8
Created a new class to manage unix shell variables.
Apr 14, 2020
9629b9b
Merge pull request #1 from Hammond95/issue/3901
Hammond95 Apr 14, 2020
9589c54
Fixed stupid bug in UnixShellEnvironmentVariable.
Apr 14, 2020
7d1f91b
Merge pull request #2 from Hammond95/issue/3901
Hammond95 Apr 14, 2020
228ea6c
Fixed bug in Script in case env_vars is None.
Apr 14, 2020
0c73db7
Merge pull request #3 from Hammond95/issue/3901
Hammond95 Apr 14, 2020
da3c6f0
Add exclusion of some characters (&, |) in the regex ENV_VAR_VALUE_RE…
Apr 14, 2020
a60e4d6
Merge pull request #5 from Hammond95/issue/3901
Hammond95 Apr 14, 2020
a651864
add file in news.
Apr 14, 2020
a69fb73
Merge pull request #6 from Hammond95/issue/3901
Hammond95 Apr 14, 2020
2565292
Merge branch 'master' into master
techalchemy Apr 23, 2020
7370fab
Merge branch 'master' into master
Hammond95 Apr 24, 2020
22eade3
Merge pull request #7 from pypa/master
Hammond95 May 3, 2020
9a3abaa
:chicken: coding.
May 3, 2020
c9bf2ad
Merge branch 'master' into issue/3901
May 3, 2020
ebdccf9
removed comments and prints
May 3, 2020
85bd47b
removed comment.
May 3, 2020
e1321ff
Merge pull request #8 from Hammond95/issue/3901
Hammond95 May 3, 2020
295dea3
Put back old behaviour in Script.parse.
May 3, 2020
a8ff046
Merge pull request #9 from Hammond95/issue/3901
Hammond95 May 3, 2020
d4929ff
changed back to single variable usage.
May 3, 2020
d61e97e
Merge pull request #10 from Hammond95/issue/3901
Hammond95 May 3, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions news/3901.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Now the `pipenv run` manage the presence of environment variables in the command (e.g. `hello=world python --version`).
At the moment this will NOT manage:
- Multiple commands execution separated by `;`, `|`, `&&`, `||`.
- The resolution of any existing defined environment variable (e.g. `hello=$world python --version` won't be resolved and will be treated as `hello='$world' python --version`)
- Environment variables setting on Windows based shell
4 changes: 2 additions & 2 deletions pipenv/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,15 +397,15 @@ def shell(
short_help="Spawns a command installed into the virtualenv.",
context_settings=subcommand_context_no_interspersion,
)
@common_options
#@common_options
@argument("command")
@argument("args", nargs=-1)
@pass_state
def run(state, command, args):
"""Spawns a command installed into the virtualenv."""
from ..core import do_run
do_run(
command=command, args=args, three=state.three, python=state.python, pypi_mirror=state.pypi_mirror
command=command, args=args, state=state,
)


Expand Down
115 changes: 112 additions & 3 deletions pipenv/cmdparse.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,77 @@
import itertools
import re
import shlex

import os
import six
import click
import crayons


class UnixShellEnvironmentVariable(object):
"""
This class provides an abstraction on
unix shell environment variables assignments.

It uses a set of regular expressions:

# ENV_VAR_NAME_REGEX matches the env var name, which requires:
- must not start with a number
- only letters, numbers and underscore allowed

# ENV_VAR_VALUE_REGEX matches a correct value for an env var:
- A value not enclosed in quote characters, must not start with "="
- A value enclosed in quotes, must end with the same quote character used to open.
- Carriage return and Newline are not allowed into the value.

# ENV_VAR_REGEX is a composition of the two previous regex:
- No spacing allowed before and after the equal sign.
"""

ENV_VAR_NAME_REGEX = r"([a-zA-Z_]+[a-zA-Z_0-9]*)"
ENV_VAR_VALUE_REGEX = \
r"((\"((?:[^\r\n\"\\]|\\.)*)\")|" + \
r"('((?:[^\r\n'\\]|\\.)*)')|" + \
r"([^\|&<>=\"\'\r\n][^\|&<>\"\'\r\n]*))"
ENV_VAR_REGEX = r"^{name_rgx}={value_rgx}".format(
name_rgx=ENV_VAR_NAME_REGEX,
value_rgx=ENV_VAR_VALUE_REGEX
)

def __init__(self, name, value, full_expr=None):
m = re.match(self.ENV_VAR_NAME_REGEX, name)
if not m:
raise ValueError("The value '{}' didn't match the ENV_VAR_NAME_REGEX!".format(name))

m = re.match(self.ENV_VAR_NAME_REGEX, value)
if not m:
raise ValueError("The value '{}' didn't match the ENV_VAR_VALUE_REGEX!".format(value))

self._name = name
self._value = value
if full_expr:
self.full_expr = full_expr
else:
self.full_expr = '{}=\'{}\''.format(name, value)

@classmethod
def parse_inline(cls, env_var_assignment):
m = re.match(cls.ENV_VAR_REGEX, env_var_assignment)
if not m:
raise ValueError("The value '{}' didn't match the ENV_VAR_REGEX!".format(env_var_assignment))

return cls(
m.group(1),
(m.group(4) or m.group(6) or m.group(7)),
full_expr=m.group(0),
)

@property
def name(self):
return self._name

@property
def value(self):
return self._value


class ScriptEmptyError(ValueError):
Expand All @@ -21,18 +90,58 @@ class Script(object):
This always works in POSIX mode, even on Windows.
"""

def __init__(self, command, args=None):
def __init__(
self,
command,
args=None,
env_vars=None
):
self._parts = [command]
if args:
self._parts.extend(args)

for env_var in (env_vars or []):
os.environ.putenv(
env_var.name,
env_var.value
)

@classmethod
def parse(cls, value):
env_vars = []
if isinstance(value, six.string_types):
value = shlex.split(value)

for el in value:
try:
env_var = UnixShellEnvironmentVariable.parse_inline(el)
env_vars.append(env_var)
value.remove(el)
click.echo(
crayons.yellow(
"WARNING: Found environment variable '{}' in command. ".format(env_var.full_expr) +
"Use env property in Pipfile instead!"
)
)
except ValueError:
pass

if isinstance(value, dict):
value = shlex.split(value['cmd'])

for k, v in (value.get('env') or {}).items():
env_vars.append(
UnixShellEnvironmentVariable(k, v)
)

if not value:
raise ScriptEmptyError(value)
return cls(value[0], value[1:])

return cls(
value[0],
args=value[1:],
env_vars=env_vars,
)

def __repr__(self):
return "Script({0!r})".format(self._parts)
Expand Down
9 changes: 6 additions & 3 deletions pipenv/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2499,7 +2499,7 @@ def do_run_posix(script, command):
)


def do_run(command, args, three=None, python=False, pypi_mirror=None):
def do_run(command, args, state):
"""Attempt to run command either pulling from project or interpreting as executable.
Args are appended to the command in [scripts] section of project if found.
Expand All @@ -2508,7 +2508,10 @@ def do_run(command, args, three=None, python=False, pypi_mirror=None):

# Ensure that virtualenv is available.
ensure_project(
three=three, python=python, validate=False, pypi_mirror=pypi_mirror,
three=state.three,
python=state.python,
validate=False,
pypi_mirror=state.pypi_mirror,
)

load_dot_env()
Expand All @@ -2528,7 +2531,7 @@ def do_run(command, args, three=None, python=False, pypi_mirror=None):
os.environ.pop("PIP_SHIMS_BASE_MODULE", None)

try:
script = project.build_script(command, args)
script = project.build_script(command, extra_args=args)
cmd_string = ' '.join([script.command] + script.args)
if environments.is_verbose():
click.echo(crayons.normal("$ {0}".format(cmd_string)), err=True)
Expand Down
4 changes: 3 additions & 1 deletion pipenv/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,9 +557,11 @@ def has_script(self, name):

def build_script(self, name, extra_args=None):
try:
# Case 1: Named execution with an alias.
script = Script.parse(self.parsed_pipfile["scripts"][name])
except KeyError:
script = Script(name)
# Case 2: A command directly provided to pipenv run.
script = Script.parse(name)
if extra_args:
script.extend(extra_args)
return script
Expand Down