Make shell evals work and add tests for that

This commit is contained in:
Joe Rickerby
2017-09-02 22:09:32 +01:00
parent f64cb35d22
commit f56ddda157
4 changed files with 67 additions and 30 deletions
+43 -15
View File
@@ -1,39 +1,67 @@
import subprocess, shlex import subprocess, shlex
from collections import namedtuple
import bashlex
NodeExecutionContext = namedtuple('NodeExecutionContext', ['environment', 'input'])
def evaluate(value, environment):
if not value:
# empty string evaluates to empty string
# (but trips up bashlex)
return ''
command_node = bashlex.parsesingle(value)
if len(command_node.parts) != 1:
raise ValueError('"%s" has too many parts' % value)
value_word_node = command_node.parts[0]
return evaluate_node(
value_word_node,
context=NodeExecutionContext(environment=environment, input=value)
)
def evaluate_node(node, environment): def evaluate_node(node, context):
if node.kind == 'word': if node.kind == 'word':
return evaluate_word_node(node, environment=environment) return evaluate_word_node(node, context=context)
elif node.kind == 'commandsubstitution': elif node.kind == 'commandsubstitution':
return evaluate_command_node(node.command, environment=environment) return evaluate_command_node(node.command, context=context)
elif node.kind == 'parameter': elif node.kind == 'parameter':
return evaluate_parameter_node(node, environment=environment) return evaluate_parameter_node(node, context=context)
else: else:
raise ValueError('Unsupported bash construct: "%s"' % node.word) raise ValueError('Unsupported bash construct: "%s"' % node.word)
def evaluate_word_node(node, environment): def evaluate_word_node(node, context):
letters = list(node.word) word_start = node.pos[0]
word_end = node.pos[1]
word_string = context.input[word_start:word_end]
letters = list(word_string)
for part in node.parts: for part in node.parts:
part_start = part.pos[0] part_start = part.pos[0] - word_start
part_end = part.pos[1] part_end = part.pos[1] - word_start
# Set all the characters in the part to None # Set all the characters in the part to None
for i in range(part_start, part_end): for i in range(part_start, part_end):
letters[i] = None letters[i] = None
letters[part_start] = evaluate_node(part, environment=environment) letters[part_start] = evaluate_node(part, context=context)
# remove the None letters and concat # remove the None letters and concat
return ''.join(l for l in letters if l is not None) value = ''.join(l for l in letters if l is not None)
# apply bash-like quotes/whitespace treatment
return ' '.join(word.strip() for word in shlex.split(value))
def evaluate_command_node(node, environment): def evaluate_command_node(node, context):
words = [evaluate_node(part, environment=environment) for part in node.parts] words = [evaluate_node(part, context=context) for part in node.parts]
command = ' '.join(words) command = ' '.join(words)
return subprocess.check_output(shlex.split(command), env=environment) return subprocess.check_output(shlex.split(command), env=context.environment)
def evaluate_parameter_node(node, environment): def evaluate_parameter_node(node, context):
return environment.get(node.value, '') return context.environment.get(node.value, '')
+1 -14
View File
@@ -35,22 +35,9 @@ class EnvironmentAssignment(object):
self.name = name self.name = name
self.value = value self.value = value
if value:
command_node = bashlex.parsesingle(value)
if len(command_node.parts) != 1:
raise ValueError('"%s" has too many parts' % value)
self.value_word_node = command_node.parts[0]
else:
self.value_word_node = None
def evaluated_value(self, environment): def evaluated_value(self, environment):
'''Returns the value of this assignment, as evaluated in the environment''' '''Returns the value of this assignment, as evaluated in the environment'''
if self.value_word_node: return bashlex_eval.evaluate(self.value, environment=environment)
return bashlex_eval.evaluate_node(self.value_word_node, environment=environment)
else:
return ''
def as_shell_assignment(self): def as_shell_assignment(self):
return 'export %s=%s' % (self.name, self.value) return 'export %s=%s' % (self.name, self.value)
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"CIBW_ENVIRONMENT": "CIBW_TEST_VAR=\"a b c\" CIBW_TEST_VAR_2=1 PATH=$PATH:/opt/cibw_test_path" "CIBW_ENVIRONMENT": "CIBW_TEST_VAR=\"a b c\" CIBW_TEST_VAR_2=1 CIBW_TEST_VAR_3=\"$(echo 'test string 3')\" PATH=$PATH:/opt/cibw_test_path "
} }
+22
View File
@@ -34,6 +34,28 @@ def test_inheritance():
assert environment_dict == {'PATH': '/usr/bin:/usr/local/bin'} assert environment_dict == {'PATH': '/usr/bin:/usr/local/bin'}
assert environment_cmds == ['export PATH=$PATH:/usr/local/bin'] assert environment_cmds == ['export PATH=$PATH:/usr/local/bin']
def test_shell_eval():
environment_recipe = parse_environment('VAR="$(echo "a test" string)"')
environment_dict = environment_recipe.as_dictionary(
prev_environment={}
)
environment_cmds = environment_recipe.as_shell_commands()
assert environment_dict == {'VAR': 'a test string'}
assert environment_cmds == ['export VAR="$(echo "a test" string)"']
def test_shell_eval_and_env():
environment_recipe = parse_environment('VAR="$(echo "$PREV_VAR" string)"')
environment_dict = environment_recipe.as_dictionary(
prev_environment={'PREV_VAR': '1 2 3'}
)
environment_cmds = environment_recipe.as_shell_commands()
assert environment_dict == {'PREV_VAR': '1 2 3', 'VAR': '1 2 3 string'}
assert environment_cmds == ['export VAR="$(echo "$PREV_VAR" string)"']
def test_empty_var(): def test_empty_var():
environment_recipe = parse_environment('CFLAGS=') environment_recipe = parse_environment('CFLAGS=')