| # -*- 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 |