import argparse import os import sys from distutils.spawn import find_executable from importlib.util import find_spec from pathlib import Path from typing import List, Optional, NoReturn, Tuple from traceback_with_variables.print import printing_exc, Format def run_script( path: Path, argv: List[str], fmt: Format, ) -> int: sys.path[0] = str(path.parent) sys.argv = [str(path)] + argv globals_ = {k: v for k, v in globals().items() if k in { '__builtins__', '__loader__', }} globals_.update({k: None for k in globals().keys() if k in { '__cached__', '__package__', '__annotations__', '__spec__', }}) globals_.update({ '__name__': '__main__', '__file__': str(path), }) with printing_exc( reraise=False, skip_cur_frame=True, fmt=fmt, ): exec(compile(path.read_text(), str(path), 'exec'), globals_, globals_) return 0 return 1 # noqa # actually 'this code is unreachable' is wrong # - parse args - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - class ParseError(RuntimeError): pass def raising_error_func(message: str) -> NoReturn: raise ParseError(message) def split_argv_to_own_and_script( raising_noabbrev_parser: argparse.ArgumentParser, # with raising .error, no abbrev argv: Optional[List[str]] = None, ) -> Tuple[List[str], List[str]]: _argv = (argv or sys.argv)[1:] for num_args in range(1, len(_argv) + 1): try: raising_noabbrev_parser.parse_args(_argv[:num_args]) except ParseError as e: if e.args[0].startswith("unrecognized arguments"): return _argv[: num_args - 1], _argv[num_args - 1:] return _argv, [] def parse_args_and_script_cmd( raising_nohelp_noabbrev_parser: argparse.ArgumentParser, # with raising .error, no help, no abbrev ) -> Tuple[argparse.Namespace, Path, List[str]]: # public_parser is only for help messages and complaints, it is always called with wrong arguments or '--help' # so it terminates the program and exits with status 1 (or 0 if called with '--help') public_parser = argparse.ArgumentParser(parents=[raising_nohelp_noabbrev_parser]) public_parser.add_argument("script") public_parser.add_argument("script-arg", nargs="*") own_argv, script_argv = split_argv_to_own_and_script( raising_nohelp_noabbrev_parser, sys.argv ) args = argparse.Namespace() # make linter happy try: args = raising_nohelp_noabbrev_parser.parse_args(own_argv) except ParseError: public_parser.parse_args(own_argv) if not script_argv: public_parser.parse_args(own_argv) if script_argv[0].startswith('-'): public_parser.parse_args(own_argv + script_argv[:1] + ['some_cmd']) if os.path.isfile(script_argv[0]): script_path_str = script_argv[0] else: script_path_str = find_executable(script_argv[0]) if not script_path_str: module_spec = find_spec(script_argv[0]) if not module_spec: public_parser.error(f"No such file or command or module: {script_argv[0]}") script_path_str = module_spec.origin return args, Path(script_path_str), script_argv[1:] def parse_args() -> Tuple[argparse.Namespace, Path, List[str]]: parser = argparse.ArgumentParser(add_help=False, allow_abbrev=False) parser.error = raising_error_func Format.add_arguments(parser) return parse_args_and_script_cmd(parser) def main(): ns, script_path, script_argv = parse_args() return run_script( path=script_path, argv=script_argv, fmt=Format.parse(ns) ) if __name__ == "__main__": sys.exit(main())