blob: 1431b0bfafbe9f337d1a9d08271a79ae68a189d9 [file] [log] [blame]
"""Utilities for analyzing and manipulating shell commands."""
import shlex
import logging
import re
import os
try:
from shlex import quote # python 3.3 and above
except ImportError:
from pipes import quote # python 3.2 and earlier
class ShellCommand:
"""This class represents a parsed shell command (a subset of posix shell
commands; see parse()). The parsed form allows direct access to information
like the executable name, working directory or the file to which
stdin/stdout/stderr is redirected."""
def __init__(self, executable=None, arguments=None):
self.stdin = None
self.stdout = None
self.stderr = None
self.executable = executable
if arguments is None:
arguments = []
self.arguments = arguments
self.workdir = None
self.envvars = {}
def toCommandline(self):
"""Transforms ShellCommand object to a posix shell commandline."""
result = ""
if self.workdir is not None:
result += "cd %s && " % quote(self.workdir)
res_list = [self.executable] + self.arguments
result += " ".join(map(quote, res_list))
for key, value in self.envvars.items():
result += "%s=%s " % (key, quote(value))
if self.stdin is not None:
result += " < %s" % quote(self.stdin)
if self.stdout is not None:
result += " > %s" % quote(self.stdout)
if self.stderr is not None:
result += " 2> %s" % quote(self.stderr)
return result
def wrap(self, new_executable, args):
"""Adds a prefix to the exeutable. Example:
Prefixing `SOMVAR=42 cd mydir && mycmd -v > /dev/null` with `lldb --`
becomes `SOMEVAR=42 cd mydir && lldb -- mycmd -v > /dev/null`."""
self.arguments = args + [self.executable] + self.arguments
self.executable = new_executable
# Tokens/keywords for shell command parsing, loosely modeled after posix
# specification for sh.
reserved_words = ['!', '{', '}', 'case', 'do', 'done', 'elif', 'else', 'esac',
'fi', 'for', 'if', 'in', 'then', 'until', 'while']
chaining_tokens = set([';', '&&', '||', '|', '&'])
unhandled_tokens = set([';;', '<<', '>>', '<&', '>&', '<>', '<<-', '>|', '(',
')'])
def parse(commandline):
"""Parses a posix shell commandline to a ShellCommand object. This supports
typical commandline with environment variables, input/output redirection
and switching directories upfront. It does not support full posix shell
and will throw an exception if the commandline uses unsupported features.
"""
result = ShellCommand()
tokens = shlex.split(commandline)
i = 0
first_word = True
while i < len(tokens):
token = tokens[i]
i += 1
if token == '<' and i < len(tokens):
result.stdin = tokens[i]
i += 1
continue
if token == '>' and i < len(tokens):
result.stdout = tokens[i]
i += 1
continue
if token == '2>' and i < len(tokens):
result.stderr = tokens[i]
i += 1
continue
if first_word:
# Is it an environment variable assignment?
assignment = re.match('([A-Za-z_][A-Za-z_0-9]*)=(.*)', token)
if assignment:
result.envvars[assignment.group(1)] = assignment.group(2)
continue
# A reserved word?
if token in reserved_words or token in unhandled_tokens:
raise Exception("Reserved word '%s' not supported" % token)
result.executable = token
first_word = False
else:
if token in chaining_tokens:
# Currently we only allow the special case of 'cd DIR ;'
if (token == ';' or token == '&&') and \
result.executable == 'cd' and \
len(result.arguments) == 1:
newresult = ShellCommand()
newresult.workdir = result.arguments[0]
result = newresult
first_word = True
continue
raise Exception("Command chaining not supported yet")
if token in unhandled_tokens or token in reserved_words:
raise Exception("Commandline with '%s' not supported yet" %
token)
result.arguments.append(token)
return result
# Some executables are just used to cleanup/prepare for a test run, ignore them
# here. This is from a time when there was no concept of a prepare script,
# it should not be necessary anymore for new test files.
_ignore_executables = set(['cd', 'rm', 'cp'])
def getMainExecutable(context):
"""Returns the main executable of the current run script. This skips over
some commands typically used to setup a benchmark (see _ignore_executables)
and returns the first executable found in the run script and prints a
warning if more than one executable is found."""
# Executable name already determined in a previous run?
if hasattr(context, 'executable'):
return context.executable
executable = None
cwd = '.'
for line in context.parsed_runscript:
cmd = parse(line)
if cmd.workdir is not None:
cwd = os.path.join(cwd, cmd.workdir)
if cmd.executable in _ignore_executables:
continue
new_executable = os.path.join(cwd, cmd.executable)
# We only support one executable yet for collecting md5sums
if new_executable != executable and executable is not None:
logging.warning("More than one executable used in test %s",
context.test.getFullName())
executable = new_executable
if executable is None:
logging.warning("No executable found for test %s",
context.test.getFullName())
context.executable = executable
return executable