blob: eac2e8c295478bb65ae75fcf9513417859c0f179 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2017 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Command line utility functions."""
from __future__ import print_function
import argparse
import logging
import os
import re
import signal
import sys
from bisect_kit import errors
from bisect_kit import util
logger = logging.getLogger(__name__)
# Exit code of bisect eval script. These values are chosen compatible with 'git
# bisect'.
EXIT_CODE_OLD = 0
EXIT_CODE_NEW = 1
EXIT_CODE_SKIP = 125
EXIT_CODE_FATAL = 128
class ArgTypeError(argparse.ArgumentTypeError):
"""An error for argument validation failure.
This not only tells users the argument is wrong but also gives correct
example. The main purpose of this error is for argtype_multiplexer, which
cascades examples from multiple ArgTypeError.
"""
def __init__(self, msg, example):
self.msg = msg
if isinstance(example, list):
self.example = example
else:
self.example = [example]
full_msg = '%s (example value: %s)' % (self.msg, ', '.join(self.example))
super(ArgTypeError, self).__init__(full_msg)
def argtype_notempty(s):
"""Validates argument is not an empty string.
Args:
s: string to validate.
Raises:
ArgTypeError if argument is empty string.
"""
if not s:
msg = 'should not be empty'
raise ArgTypeError(msg, 'foo')
return s
def argtype_int(s):
"""Validate argument is a number.
Args:
s: string to validate.
Raises:
ArgTypeError if argument is not a number.
"""
try:
return str(int(s))
except ValueError:
raise ArgTypeError('should be a number', '123')
def argtype_re(pattern, example):
r"""Validate argument matches `pattern`.
Args:
pattern: regex pattern
example: example string which matches `pattern`
Returns:
A new argtype function which matches regex `pattern`
"""
assert re.match(pattern, example)
def validate(s):
if re.match(pattern, s):
return s
if re.escape(pattern) == pattern:
raise ArgTypeError('should be "%s"' % pattern, pattern)
raise ArgTypeError('should match "%s"' % pattern,
'"%s" like %s' % (pattern, example))
return validate
def argtype_multiplexer(*args):
r"""argtype multiplexer
This function takes a list of argtypes and creates a new function matching
them. Moreover, it gives error message with examples.
Examples:
>>> argtype = argtype_multiplexer(argtype_int,
argtype_re(r'^r\d+$', 'r123'))
>>> argtype('123')
123
>>> argtype('r456')
r456
>>> argtype('hello')
ArgTypeError: Invalid argument (example value: 123, "r\d+$" like r123)
Args:
*args: list of argtypes or regex pattern.
Returns:
A new argtype function which matches *args.
"""
def validate(s):
examples = []
for t in args:
try:
return t(s)
except ArgTypeError as e:
examples += e.example
msg = 'Invalid argument'
raise ArgTypeError(msg, examples)
return validate
def argtype_multiplier(argtype):
"""A new argtype that supports multiplier suffix of the given argtype.
Examples:
Supports the given argtype accepting "foo" as argument, this function
generates a new argtype function which accepts argument like "foo*3".
Returns:
A new argtype function which returns (arg, times) where arg is accepted
by input `argtype` and times is repeating count. Note that if multiplier is
omitted, "times" is 1.
"""
def helper(s):
m = re.match(r'^(.+)\*(\d+)$', s)
try:
if m:
return argtype(m.group(1)), int(m.group(2))
return argtype(s), 1
except ArgTypeError as e:
# It should be okay to gives multiplier example only for the first one
# because it is just "example", no need to enumerate all possibilities.
raise ArgTypeError(e.msg, e.example + [e.example[0] + '*3'])
return helper
def argtype_dir_path(s):
"""Validate argument is an existing directory.
Args:
s: string to validate.
Raises:
ArgTypeError if the path is not a directory.
"""
if not os.path.exists(s):
raise ArgTypeError('should be an existing directory', '/path/to/somewhere')
if not os.path.isdir(s):
raise ArgTypeError('should be a directory', '/path/to/somewhere')
# Normalize, trim trailing path separators.
if len(s) > 1 and s[-1] == os.path.sep:
s = s[:-1]
return s
def check_executable(program):
"""Checks whether a program is executable.
Args:
program: program path in question
Returns:
string as error message if `program` is not executable, or None otherwise.
It will return None if unable to determine as well.
"""
returncode = util.call('which', program)
if returncode == 127: # No 'which' on this platform, skip the check.
return None
if returncode == 0: # is executable
return None
hint = ''
if not os.path.exists(program):
hint = 'Not in PATH?'
elif not os.path.isfile(program):
hint = 'Not a file'
elif not os.access(program, os.X_OK):
hint = 'Forgot to chmod +x?'
elif '/' not in program:
hint = 'Forgot to prepend "./" ?'
return '%r is not executable. %s' % (program, hint)
def lookup_signal_name(signum):
"""Look up signal name by signal number.
Args:
signum: signal number
Returns:
signal name, like "SIGTERM". "Unknown" for unexpected number.
"""
for k, v in vars(signal).items():
if k.startswith('SIG') and '_' not in k and v == signum:
return k
return 'Unknown'
def format_returncode(returncode):
# returncode is negative if the process is terminated by signal directly.
if returncode < 0:
signum = -returncode
signame = lookup_signal_name(signum)
return 'terminated by signal %d (%s)' % (signum, signame)
# Some programs, e.g. shell, handled signal X and exited with (128 + X).
if returncode > 128:
signum = returncode - 128
signame = lookup_signal_name(signum)
return 'exited with code %d; may be signal %d (%s)' % (returncode, signum,
signame)
return 'exited with code %d' % returncode
def patching_argparser_exit(parser):
"""Patching argparse.ArgumentParser.exit to exit with fatal exit code.
Args:
parser: argparse.ArgumentParser object
"""
orig_exit = parser.exit
def exit_hack(status=0, message=None):
if status != 0:
status = EXIT_CODE_FATAL
orig_exit(status, message)
parser.exit = exit_hack
def fatal_error_handler(func):
"""Function decorator which exits with fatal code for fatal exceptions.
This is a helper for switcher and evaluator. It catches fatal exceptions and
exits with fatal exit code. The fatal exit code (128) is aligned with 'git
bisect'.
See also argparse_fatal_error().
Args:
func: wrapped function
"""
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except (AssertionError, errors.ExecutionFatalError, errors.ArgumentError):
logger.exception('fatal exception, bisection should stop')
sys.exit(EXIT_CODE_FATAL)
return wrapper