blob: f23a984ca3555145adbc4cc4b7928b810344af84 [file] [log] [blame]
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from __future__ import print_function
import pipes
import subprocess
import sys
COLOR_ANSI_CODE_MAP = {
'black': 90,
'red': 91,
'green': 92,
'yellow': 93,
'blue': 94,
'magenta': 95,
'cyan': 96,
'white': 97,
}
def Colored(message, color):
"""Wraps the message into ASCII color escape codes.
Args:
message: Message to be wrapped.
color: See COLOR_ANSI_CODE_MAP.keys() for available choices.
"""
# This only works on Linux and OS X. Windows users must install ANSICON or
# use VT100 emulation on Windows 10.
assert color in COLOR_ANSI_CODE_MAP, 'Unsupported color'
return '\033[%dm%s\033[0m' % (COLOR_ANSI_CODE_MAP[color], message)
def Info(message, **kwargs):
print(message.format(**kwargs))
def Comment(message, **kwargs):
"""Prints an import message to the user."""
print(Colored(message.format(**kwargs), 'yellow'))
def Fatal(message, **kwargs):
"""Displays an error to the user and terminates the program."""
Error(message, **kwargs)
sys.exit(1)
def Error(message, **kwargs):
"""Displays an error to the user."""
print(Colored(message.format(**kwargs), 'red'))
def Step(name):
"""Display a decorated message to the user.
This is useful to separate major stages of the script. For simple messages,
please use comment function above.
"""
boundary = max(80, len(name))
print(Colored('=' * boundary, 'green'))
print(Colored(name, 'green'))
print(Colored('=' * boundary, 'green'))
def Ask(question, answers=None, default=None):
"""Asks the user to answer a question with multiple choices.
Users are able to press Return to access the default answer (if specified) and
to type part of the full answer, e.g. "y", "ye" or "yes" are all valid answers
for "yes". The func will ask user again in case an invalid answer is provided.
Raises ValueError if default is specified, but not listed an a valid answer.
Args:
question: Question to be asked.
answers: List or dictinary describing user choices. In case of a dictionary,
the keys are the options display to the user and values are the return
values for this method. In case of a list, returned values are same as
options displayed to the user. Defaults to {'yes': True, 'no', False}.
default: Default option chosen on empty answer. Defaults to 'yes' if default
value is used for answers parameter or to lack of default answer
otherwise.
Returns:
Chosen option from answers. Full option name is returned even if user only
enters part of it or chooses the default.
"""
if answers is None:
answers = {'yes': True, 'no': False}
default = 'yes'
if isinstance(answers, list):
answers = {v: v for v in answers}
# Generate a set of prefixes for all answers such that the user can type just
# the minimum number of characters required, e.g. 'y' or 'ye' can be used for
# the 'yes' answer. Shared prefixes are ignored, e.g. 'n' and 'ne' will not be
# accepted if 'negate' and 'next' are both valid answers, whereas 'nex' and
# 'neg' would be accepted.
inputs = {}
common_prefixes = set()
for ans, retval in answers.iteritems():
for i in range(len(ans)):
inp = ans[:i+1]
if inp in inputs:
common_prefixes.add(inp)
del inputs[inp]
if inp not in common_prefixes:
inputs[inp] = retval
if default is None:
prompt = ' [%s] ' % '/'.join(answers)
elif default in answers:
ans_with_def = [a if a != default else a.upper()
for a in sorted(answers.keys())]
prompt = ' [%s] ' % '/'.join(ans_with_def)
else:
raise ValueError('invalid default answer: "%s"' % default)
while True:
print(Colored(question + prompt, 'cyan'), end=' ')
choice = raw_input().strip().lower()
if default is not None and choice == '':
return inputs[default]
elif choice in inputs:
return inputs[choice]
else:
choices = sorted(['"%s"' % a for a in sorted(answers.keys())])
Error('Please respond with %s or %s.' % (
', '.join(choices[:-1]), choices[-1]))
def CheckLog(command, log_path, env=None):
"""Executes a command and writes its stdout to a specified log file.
On non-zero return value, also prints the content of the file to the screen
and raises subprocess.CalledProcessError.
Args:
command: Command to be run as a list of arguments.
log_path: Path to a file to which the output will be written.
env: Environment to run the command in.
"""
with open(log_path, 'w') as f:
try:
cmd_str = (' '.join(pipes.quote(c) for c in command)
if isinstance(command, list) else command)
print(Colored(cmd_str, 'blue'))
print(Colored('Logging stdout & stderr to %s' % log_path, 'blue'))
subprocess.check_call(
command, stdout=f, stderr=subprocess.STDOUT, shell=False, env=env)
except subprocess.CalledProcessError:
Error('=' * 80)
Error('Received non-zero return code. Log content:')
Error('=' * 80)
subprocess.call(['cat', log_path])
Error('=' * 80)
raise
def Run(command, ok_fail=False, **kwargs):
"""Prints and runs the command. Allows to ignore non-zero exit code."""
if not isinstance(command, list):
raise ValueError('command must be a list')
print(Colored(' '.join(pipes.quote(c) for c in command), 'blue'))
try:
return subprocess.check_call(command, **kwargs)
except subprocess.CalledProcessError as cpe:
if not ok_fail:
raise
else:
return cpe.returncode