Path: blob/main/Tools/c-analyzer/c_common/scriptutil.py
12 views
import argparse1import contextlib2import logging3import os4import os.path5import shutil6import sys78from . import fsutil, strutil, iterutil, logging as loggingutil91011_NOT_SET = object()121314def get_prog(spec=None, *, absolute=False, allowsuffix=True):15if spec is None:16_, spec = _find_script()17# This is more natural for prog than __file__ would be.18filename = sys.argv[0]19elif isinstance(spec, str):20filename = os.path.normpath(spec)21spec = None22else:23filename = spec.origin24if _is_standalone(filename):25# Check if "installed".26if allowsuffix or not filename.endswith('.py'):27basename = os.path.basename(filename)28found = shutil.which(basename)29if found:30script = os.path.abspath(filename)31found = os.path.abspath(found)32if os.path.normcase(script) == os.path.normcase(found):33return basename34# It is only "standalone".35if absolute:36filename = os.path.abspath(filename)37return filename38elif spec is not None:39module = spec.name40if module.endswith('.__main__'):41module = module[:-9]42return f'{sys.executable} -m {module}'43else:44if absolute:45filename = os.path.abspath(filename)46return f'{sys.executable} {filename}'474849def _find_script():50frame = sys._getframe(2)51while frame.f_globals['__name__'] != '__main__':52frame = frame.f_back5354# This should match sys.argv[0].55filename = frame.f_globals['__file__']56# This will be None if -m wasn't used..57spec = frame.f_globals['__spec__']58return filename, spec596061def is_installed(filename, *, allowsuffix=True):62if not allowsuffix and filename.endswith('.py'):63return False64filename = os.path.abspath(os.path.normalize(filename))65found = shutil.which(os.path.basename(filename))66if not found:67return False68if found != filename:69return False70return _is_standalone(filename)717273def is_standalone(filename):74filename = os.path.abspath(os.path.normalize(filename))75return _is_standalone(filename)767778def _is_standalone(filename):79return fsutil.is_executable(filename)808182##################################83# logging8485VERBOSITY = 38687TRACEBACK = os.environ.get('SHOW_TRACEBACK', '').strip()88TRACEBACK = bool(TRACEBACK and TRACEBACK.upper() not in ('0', 'FALSE', 'NO'))899091logger = logging.getLogger(__name__)929394def configure_logger(verbosity, logger=None, **kwargs):95if logger is None:96# Configure the root logger.97logger = logging.getLogger()98loggingutil.configure_logger(logger, verbosity, **kwargs)99100101##################################102# selections103104class UnsupportedSelectionError(Exception):105def __init__(self, values, possible):106self.values = tuple(values)107self.possible = tuple(possible)108super().__init__(f'unsupported selections {self.unique}')109110@property111def unique(self):112return tuple(sorted(set(self.values)))113114115def normalize_selection(selected: str, *, possible=None):116if selected in (None, True, False):117return selected118elif isinstance(selected, str):119selected = [selected]120elif not selected:121return ()122123unsupported = []124_selected = set()125for item in selected:126if not item:127continue128for value in item.strip().replace(',', ' ').split():129if not value:130continue131# XXX Handle subtraction (leading "-").132if possible and value not in possible and value != 'all':133unsupported.append(value)134_selected.add(value)135if unsupported:136raise UnsupportedSelectionError(unsupported, tuple(possible))137if 'all' in _selected:138return True139return frozenset(selected)140141142##################################143# CLI parsing helpers144145class CLIArgSpec(tuple):146def __new__(cls, *args, **kwargs):147return super().__new__(cls, (args, kwargs))148149def __repr__(self):150args, kwargs = self151args = [repr(arg) for arg in args]152for name, value in kwargs.items():153args.append(f'{name}={value!r}')154return f'{type(self).__name__}({", ".join(args)})'155156def __call__(self, parser, *, _noop=(lambda a: None)):157self.apply(parser)158return _noop159160def apply(self, parser):161args, kwargs = self162parser.add_argument(*args, **kwargs)163164165def apply_cli_argspecs(parser, specs):166processors = []167for spec in specs:168if callable(spec):169procs = spec(parser)170_add_procs(processors, procs)171else:172args, kwargs = spec173parser.add_argument(args, kwargs)174return processors175176177def _add_procs(flattened, procs):178# XXX Fail on non-empty, non-callable procs?179if not procs:180return181if callable(procs):182flattened.append(procs)183else:184#processors.extend(p for p in procs if callable(p))185for proc in procs:186_add_procs(flattened, proc)187188189def add_verbosity_cli(parser):190parser.add_argument('-q', '--quiet', action='count', default=0)191parser.add_argument('-v', '--verbose', action='count', default=0)192193def process_args(args, *, argv=None):194ns = vars(args)195key = 'verbosity'196if key in ns:197parser.error(f'duplicate arg {key!r}')198ns[key] = max(0, VERBOSITY + ns.pop('verbose') - ns.pop('quiet'))199return key200return process_args201202203def add_traceback_cli(parser):204parser.add_argument('--traceback', '--tb', action='store_true',205default=TRACEBACK)206parser.add_argument('--no-traceback', '--no-tb', dest='traceback',207action='store_const', const=False)208209def process_args(args, *, argv=None):210ns = vars(args)211key = 'traceback_cm'212if key in ns:213parser.error(f'duplicate arg {key!r}')214showtb = ns.pop('traceback')215216@contextlib.contextmanager217def traceback_cm():218restore = loggingutil.hide_emit_errors()219try:220yield221except BrokenPipeError:222# It was piped to "head" or something similar.223pass224except NotImplementedError:225raise # re-raise226except Exception as exc:227if not showtb:228sys.exit(f'ERROR: {exc}')229raise # re-raise230except KeyboardInterrupt:231if not showtb:232sys.exit('\nINTERRUPTED')233raise # re-raise234except BaseException as exc:235if not showtb:236sys.exit(f'{type(exc).__name__}: {exc}')237raise # re-raise238finally:239restore()240ns[key] = traceback_cm()241return key242return process_args243244245def add_sepval_cli(parser, opt, dest, choices, *, sep=',', **kwargs):246# if opt is True:247# parser.add_argument(f'--{dest}', action='append', **kwargs)248# elif isinstance(opt, str) and opt.startswith('-'):249# parser.add_argument(opt, dest=dest, action='append', **kwargs)250# else:251# arg = dest if not opt else opt252# kwargs.setdefault('nargs', '+')253# parser.add_argument(arg, dest=dest, action='append', **kwargs)254if not isinstance(opt, str):255parser.error(f'opt must be a string, got {opt!r}')256elif opt.startswith('-'):257parser.add_argument(opt, dest=dest, action='append', **kwargs)258else:259kwargs.setdefault('nargs', '+')260#kwargs.setdefault('metavar', opt.upper())261parser.add_argument(opt, dest=dest, action='append', **kwargs)262263def process_args(args, *, argv=None):264ns = vars(args)265266# XXX Use normalize_selection()?267if isinstance(ns[dest], str):268ns[dest] = [ns[dest]]269selections = []270for many in ns[dest] or ():271for value in many.split(sep):272if value not in choices:273parser.error(f'unknown {dest} {value!r}')274selections.append(value)275ns[dest] = selections276return process_args277278279def add_files_cli(parser, *, excluded=None, nargs=None):280process_files = add_file_filtering_cli(parser, excluded=excluded)281parser.add_argument('filenames', nargs=nargs or '+', metavar='FILENAME')282return [283process_files,284]285286287def add_file_filtering_cli(parser, *, excluded=None):288parser.add_argument('--start')289parser.add_argument('--include', action='append')290parser.add_argument('--exclude', action='append')291292excluded = tuple(excluded or ())293294def process_args(args, *, argv=None):295ns = vars(args)296key = 'iter_filenames'297if key in ns:298parser.error(f'duplicate arg {key!r}')299300_include = tuple(ns.pop('include') or ())301_exclude = excluded + tuple(ns.pop('exclude') or ())302kwargs = dict(303start=ns.pop('start'),304include=tuple(_parse_files(_include)),305exclude=tuple(_parse_files(_exclude)),306# We use the default for "show_header"307)308def process_filenames(filenames, relroot=None):309return fsutil.process_filenames(filenames, relroot=relroot, **kwargs)310ns[key] = process_filenames311return process_args312313314def _parse_files(filenames):315for filename, _ in strutil.parse_entries(filenames):316yield filename.strip()317318319def add_progress_cli(parser, *, threshold=VERBOSITY, **kwargs):320parser.add_argument('--progress', dest='track_progress', action='store_const', const=True)321parser.add_argument('--no-progress', dest='track_progress', action='store_false')322parser.set_defaults(track_progress=True)323324def process_args(args, *, argv=None):325if args.track_progress:326ns = vars(args)327verbosity = ns.get('verbosity', VERBOSITY)328if verbosity <= threshold:329args.track_progress = track_progress_compact330else:331args.track_progress = track_progress_flat332return process_args333334335def add_failure_filtering_cli(parser, pool, *, default=False):336parser.add_argument('--fail', action='append',337metavar=f'"{{all|{"|".join(sorted(pool))}}},..."')338parser.add_argument('--no-fail', dest='fail', action='store_const', const=())339340def process_args(args, *, argv=None):341ns = vars(args)342343fail = ns.pop('fail')344try:345fail = normalize_selection(fail, possible=pool)346except UnsupportedSelectionError as exc:347parser.error(f'invalid --fail values: {", ".join(exc.unique)}')348else:349if fail is None:350fail = default351352if fail is True:353def ignore_exc(_exc):354return False355elif fail is False:356def ignore_exc(_exc):357return True358else:359def ignore_exc(exc):360for err in fail:361if type(exc) == pool[err]:362return False363else:364return True365args.ignore_exc = ignore_exc366return process_args367368369def add_kind_filtering_cli(parser, *, default=None):370parser.add_argument('--kinds', action='append')371372def process_args(args, *, argv=None):373ns = vars(args)374375kinds = []376for kind in ns.pop('kinds') or default or ():377kinds.extend(kind.strip().replace(',', ' ').split())378379if not kinds:380match_kind = (lambda k: True)381else:382included = set()383excluded = set()384for kind in kinds:385if kind.startswith('-'):386kind = kind[1:]387excluded.add(kind)388if kind in included:389included.remove(kind)390else:391included.add(kind)392if kind in excluded:393excluded.remove(kind)394if excluded:395if included:396... # XXX fail?397def match_kind(kind, *, _excluded=excluded):398return kind not in _excluded399else:400def match_kind(kind, *, _included=included):401return kind in _included402args.match_kind = match_kind403return process_args404405406COMMON_CLI = [407add_verbosity_cli,408add_traceback_cli,409#add_dryrun_cli,410]411412413def add_commands_cli(parser, commands, *, commonspecs=COMMON_CLI, subset=None):414arg_processors = {}415if isinstance(subset, str):416cmdname = subset417try:418_, argspecs, _ = commands[cmdname]419except KeyError:420raise ValueError(f'unsupported subset {subset!r}')421parser.set_defaults(cmd=cmdname)422arg_processors[cmdname] = _add_cmd_cli(parser, commonspecs, argspecs)423else:424if subset is None:425cmdnames = subset = list(commands)426elif not subset:427raise NotImplementedError428elif isinstance(subset, set):429cmdnames = [k for k in commands if k in subset]430subset = sorted(subset)431else:432cmdnames = [n for n in subset if n in commands]433if len(cmdnames) < len(subset):434bad = tuple(n for n in subset if n not in commands)435raise ValueError(f'unsupported subset {bad}')436437common = argparse.ArgumentParser(add_help=False)438common_processors = apply_cli_argspecs(common, commonspecs)439subs = parser.add_subparsers(dest='cmd')440for cmdname in cmdnames:441description, argspecs, _ = commands[cmdname]442sub = subs.add_parser(443cmdname,444description=description,445parents=[common],446)447cmd_processors = _add_cmd_cli(sub, (), argspecs)448arg_processors[cmdname] = common_processors + cmd_processors449return arg_processors450451452def _add_cmd_cli(parser, commonspecs, argspecs):453processors = []454argspecs = list(commonspecs or ()) + list(argspecs or ())455for argspec in argspecs:456if callable(argspec):457procs = argspec(parser)458_add_procs(processors, procs)459else:460if not argspec:461raise NotImplementedError462args = list(argspec)463if not isinstance(args[-1], str):464kwargs = args.pop()465if not isinstance(args[0], str):466try:467args, = args468except (TypeError, ValueError):469parser.error(f'invalid cmd args {argspec!r}')470else:471kwargs = {}472parser.add_argument(*args, **kwargs)473# There will be nothing to process.474return processors475476477def _flatten_processors(processors):478for proc in processors:479if proc is None:480continue481if callable(proc):482yield proc483else:484yield from _flatten_processors(proc)485486487def process_args(args, argv, processors, *, keys=None):488processors = _flatten_processors(processors)489ns = vars(args)490extracted = {}491if keys is None:492for process_args in processors:493for key in process_args(args, argv=argv):494extracted[key] = ns.pop(key)495else:496remainder = set(keys)497for process_args in processors:498hanging = process_args(args, argv=argv)499if isinstance(hanging, str):500hanging = [hanging]501for key in hanging or ():502if key not in remainder:503raise NotImplementedError(key)504extracted[key] = ns.pop(key)505remainder.remove(key)506if remainder:507raise NotImplementedError(sorted(remainder))508return extracted509510511def process_args_by_key(args, argv, processors, keys):512extracted = process_args(args, argv, processors, keys=keys)513return [extracted[key] for key in keys]514515516##################################517# commands518519def set_command(name, add_cli):520"""A decorator factory to set CLI info."""521def decorator(func):522if hasattr(func, '__cli__'):523raise Exception(f'already set')524func.__cli__ = (name, add_cli)525return func526return decorator527528529##################################530# main() helpers531532def filter_filenames(filenames, process_filenames=None, relroot=fsutil.USE_CWD):533# We expect each filename to be a normalized, absolute path.534for filename, _, check, _ in _iter_filenames(filenames, process_filenames, relroot):535if (reason := check()):536logger.debug(f'{filename}: {reason}')537continue538yield filename539540541def main_for_filenames(filenames, process_filenames=None, relroot=fsutil.USE_CWD):542filenames, relroot = fsutil.fix_filenames(filenames, relroot=relroot)543for filename, relfile, check, show in _iter_filenames(filenames, process_filenames, relroot):544if show:545print()546print(relfile)547print('-------------------------------------------')548if (reason := check()):549print(reason)550continue551yield filename, relfile552553554def _iter_filenames(filenames, process, relroot):555if process is None:556yield from fsutil.process_filenames(filenames, relroot=relroot)557return558559onempty = Exception('no filenames provided')560items = process(filenames, relroot=relroot)561items, peeked = iterutil.peek_and_iter(items)562if not items:563raise onempty564if isinstance(peeked, str):565if relroot and relroot is not fsutil.USE_CWD:566relroot = os.path.abspath(relroot)567check = (lambda: True)568for filename, ismany in iterutil.iter_many(items, onempty):569relfile = fsutil.format_filename(filename, relroot, fixroot=False)570yield filename, relfile, check, ismany571elif len(peeked) == 4:572yield from items573else:574raise NotImplementedError575576577def track_progress_compact(items, *, groups=5, **mark_kwargs):578last = os.linesep579marks = iter_marks(groups=groups, **mark_kwargs)580for item in items:581last = next(marks)582print(last, end='', flush=True)583yield item584if not last.endswith(os.linesep):585print()586587588def track_progress_flat(items, fmt='<{}>'):589for item in items:590print(fmt.format(item), flush=True)591yield item592593594def iter_marks(mark='.', *, group=5, groups=2, lines=_NOT_SET, sep=' '):595mark = mark or ''596group = group if group and group > 1 else 1597groups = groups if groups and groups > 1 else 1598599sep = f'{mark}{sep}' if sep else mark600end = f'{mark}{os.linesep}'601div = os.linesep602perline = group * groups603if lines is _NOT_SET:604# By default we try to put about 100 in each line group.605perlines = 100 // perline * perline606elif not lines or lines < 0:607perlines = None608else:609perlines = perline * lines610611if perline == 1:612yield end613elif group == 1:614yield sep615616count = 1617while True:618if count % perline == 0:619yield end620if perlines and count % perlines == 0:621yield div622elif count % group == 0:623yield sep624else:625yield mark626count += 1627628629