blob: 21c409a23e7502d034f9a8993e84bb1bf410386f [file] [log] [blame]
Kuang-che Wu6e4beca2018-06-27 17:45:02 +08001# -*- coding: utf-8 -*-
Kuang-che Wu88875db2017-07-20 10:47:53 +08002# Copyright 2017 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
Kuang-che Wu2526a672019-09-10 16:23:59 +08005"""Command line utility functions."""
Kuang-che Wu88875db2017-07-20 10:47:53 +08006
7from __future__ import print_function
8import argparse
Kuang-che Wu88875db2017-07-20 10:47:53 +08009import logging
10import os
11import re
Kuang-che Wu443633f2019-02-27 00:58:33 +080012import signal
Kuang-che Wu88875db2017-07-20 10:47:53 +080013
Kuang-che Wu88875db2017-07-20 10:47:53 +080014from bisect_kit import util
15
16logger = logging.getLogger(__name__)
17
Kuang-che Wu0476d1f2019-03-04 19:27:01 +080018# Exit code of bisect eval script. These values are chosen compatible with 'git
19# bisect'.
20EXIT_CODE_OLD = 0
21EXIT_CODE_NEW = 1
22EXIT_CODE_SKIP = 125
23EXIT_CODE_FATAL = 128
24
Kuang-che Wu88875db2017-07-20 10:47:53 +080025
26class ArgTypeError(argparse.ArgumentTypeError):
27 """An error for argument validation failure.
28
29 This not only tells users the argument is wrong but also gives correct
30 example. The main purpose of this error is for argtype_multiplexer, which
31 cascades examples from multiple ArgTypeError.
32 """
33
34 def __init__(self, msg, example):
35 self.msg = msg
36 if isinstance(example, list):
37 self.example = example
38 else:
39 self.example = [example]
40 full_msg = '%s (example value: %s)' % (self.msg, ', '.join(self.example))
41 super(ArgTypeError, self).__init__(full_msg)
42
43
44def argtype_notempty(s):
45 """Validates argument is not an empty string.
46
47 Args:
48 s: string to validate.
49
50 Raises:
51 ArgTypeError if argument is empty string.
52 """
53 if not s:
54 msg = 'should not be empty'
55 raise ArgTypeError(msg, 'foo')
56 return s
57
58
59def argtype_int(s):
60 """Validate argument is a number.
61
62 Args:
63 s: string to validate.
64
65 Raises:
66 ArgTypeError if argument is not a number.
67 """
68 try:
69 return str(int(s))
70 except ValueError:
71 raise ArgTypeError('should be a number', '123')
72
73
Kuang-che Wu603cdad2019-01-18 21:32:55 +080074def argtype_re(pattern, example):
75 r"""Validate argument matches `pattern`.
76
77 Args:
78 pattern: regex pattern
79 example: example string which matches `pattern`
80
81 Returns:
82 A new argtype function which matches regex `pattern`
83 """
84 assert re.match(pattern, example)
85
86 def validate(s):
87 if re.match(pattern, s):
88 return s
89 if re.escape(pattern) == pattern:
90 raise ArgTypeError('should be "%s"' % pattern, pattern)
91 raise ArgTypeError('should match "%s"' % pattern,
92 '"%s" like %s' % (pattern, example))
93
94 return validate
95
96
Kuang-che Wu88875db2017-07-20 10:47:53 +080097def argtype_multiplexer(*args):
98 r"""argtype multiplexer
99
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800100 This function takes a list of argtypes and creates a new function matching
101 them. Moreover, it gives error message with examples.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800102
Kuang-che Wubaaa4532018-08-15 17:08:10 +0800103 Examples:
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800104 >>> argtype = argtype_multiplexer(argtype_int,
105 argtype_re(r'^r\d+$', 'r123'))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800106 >>> argtype('123')
107 123
108 >>> argtype('r456')
109 r456
110 >>> argtype('hello')
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800111 ArgTypeError: Invalid argument (example value: 123, "r\d+$" like r123)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800112
113 Args:
114 *args: list of argtypes or regex pattern.
115
116 Returns:
117 A new argtype function which matches *args.
118 """
119
120 def validate(s):
121 examples = []
122 for t in args:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800123 try:
124 return t(s)
125 except ArgTypeError as e:
126 examples += e.example
127
128 msg = 'Invalid argument'
129 raise ArgTypeError(msg, examples)
130
131 return validate
132
133
134def argtype_multiplier(argtype):
135 """A new argtype that supports multiplier suffix of the given argtype.
136
Kuang-che Wubaaa4532018-08-15 17:08:10 +0800137 Examples:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800138 Supports the given argtype accepting "foo" as argument, this function
139 generates a new argtype function which accepts argument like "foo*3".
140
141 Returns:
142 A new argtype function which returns (arg, times) where arg is accepted
143 by input `argtype` and times is repeating count. Note that if multiplier is
144 omitted, "times" is 1.
145 """
146
147 def helper(s):
148 m = re.match(r'^(.+)\*(\d+)$', s)
149 try:
150 if m:
151 return argtype(m.group(1)), int(m.group(2))
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800152 return argtype(s), 1
Kuang-che Wu88875db2017-07-20 10:47:53 +0800153 except ArgTypeError as e:
154 # It should be okay to gives multiplier example only for the first one
155 # because it is just "example", no need to enumerate all possibilities.
156 raise ArgTypeError(e.msg, e.example + [e.example[0] + '*3'])
157
158 return helper
159
160
161def argtype_dir_path(s):
162 """Validate argument is an existing directory.
163
164 Args:
165 s: string to validate.
166
167 Raises:
168 ArgTypeError if the path is not a directory.
169 """
170 if not os.path.exists(s):
171 raise ArgTypeError('should be an existing directory', '/path/to/somewhere')
172 if not os.path.isdir(s):
173 raise ArgTypeError('should be a directory', '/path/to/somewhere')
174
175 # Normalize, trim trailing path separators.
176 if len(s) > 1 and s[-1] == os.path.sep:
177 s = s[:-1]
178 return s
179
180
Kuang-che Wu88518882017-09-22 16:57:25 +0800181def check_executable(program):
182 """Checks whether a program is executable.
183
184 Args:
185 program: program path in question
186
187 Returns:
188 string as error message if `program` is not executable, or None otherwise.
189 It will return None if unable to determine as well.
190 """
191 returncode = util.call('which', program)
192 if returncode == 127: # No 'which' on this platform, skip the check.
193 return None
194 if returncode == 0: # is executable
195 return None
196
197 hint = ''
198 if not os.path.exists(program):
199 hint = 'Not in PATH?'
200 elif not os.path.isfile(program):
201 hint = 'Not a file'
202 elif not os.access(program, os.X_OK):
203 hint = 'Forgot to chmod +x?'
204 elif '/' not in program:
205 hint = 'Forgot to prepend "./" ?'
206 return '%r is not executable. %s' % (program, hint)
207
208
Kuang-che Wubc7bfce2019-05-21 18:43:16 +0800209def lookup_signal_name(signum):
210 """Look up signal name by signal number.
211
212 Args:
213 signum: signal number
214
215 Returns:
216 signal name, like "SIGTERM". "Unknown" for unexpected number.
217 """
218 for k, v in vars(signal).items():
219 if k.startswith('SIG') and '_' not in k and v == signum:
220 return k
221 return 'Unknown'
222
223
Kuang-che Wu443633f2019-02-27 00:58:33 +0800224def format_returncode(returncode):
Kuang-che Wubc7bfce2019-05-21 18:43:16 +0800225 # returncode is negative if the process is terminated by signal directly.
Kuang-che Wu443633f2019-02-27 00:58:33 +0800226 if returncode < 0:
227 signum = -returncode
Kuang-che Wubc7bfce2019-05-21 18:43:16 +0800228 signame = lookup_signal_name(signum)
Kuang-che Wu443633f2019-02-27 00:58:33 +0800229 return 'terminated by signal %d (%s)' % (signum, signame)
Kuang-che Wubc7bfce2019-05-21 18:43:16 +0800230 # Some programs, e.g. shell, handled signal X and exited with (128 + X).
231 if returncode > 128:
232 signum = returncode - 128
233 signame = lookup_signal_name(signum)
234 return 'exited with code %d; may be signal %d (%s)' % (returncode, signum,
235 signame)
Kuang-che Wu443633f2019-02-27 00:58:33 +0800236
237 return 'exited with code %d' % returncode