Last active
March 10, 2022 16:11
-
-
Save loicmolinari/be1488c067055c64a32b9b116b19f2b8 to your computer and use it in GitHub Desktop.
Boilerplate Python script for a CLI that reads from an input file and writes to an output file with default files set to stdin and stdout and with standard option names. Provides basic support for parsing command-line parameters of different types (integers, reals, flags, positional parameters, etc).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import os | |
| import sys | |
| from typing import Tuple, List, Iterator, IO, Callable, Any | |
| # Terminal ECMA-48 SGR escape sequences. | |
| ECMA48_BOLD: str = '\x1b[01m' | |
| ECMA48_RESET: str = '\x1b[00m' | |
| HAS_ECMA48_SGR: bool = not os.getenv('NO_COLOR') and (os.getenv('TERM') != 'dumb') \ | |
| and sys.stdout.isatty() | |
| def usage(command: str) -> None: | |
| command_str: str = (ECMA48_BOLD + 'Command' + ECMA48_RESET) if HAS_ECMA48_SGR else 'Command' | |
| options_str: str = (ECMA48_BOLD + 'Options' + ECMA48_RESET) if HAS_ECMA48_SGR else 'Options' | |
| print(f'Command description.\n' | |
| f'\n' | |
| f'{command_str}\n' | |
| f' {command} [-h] [-|<file>] [-c|-o <file>] [-f] [-i <value>]\n' | |
| f'\n' | |
| f'{options_str}\n' | |
| f' -h, --help Show this help.\n' | |
| f' -, --stdin Read from stdin (default).\n' | |
| f' -c, --stdout Write to stdout (default).\n' | |
| f' -o, --output <file> Write to <file>.\n' | |
| f' -f, --force Overwrite existing output file.\n' | |
| f' -i, --integer <value> An integer value.\n' | |
| f' -r, --real <value> A real value.') | |
| def from_str(string: str, func: Callable, default: Any = 0) -> Any: | |
| try: | |
| return func(string) | |
| except: | |
| return default | |
| def parse_command_line(args: List[str]) -> Tuple[str, str, bool, int, float, List[str]]: | |
| input_filename: str = '' | |
| output_filename: str = '' | |
| force: bool = False | |
| integer: int = 0 | |
| real: float = 0 | |
| positional: List[str] = [] | |
| args_iterator: Iterator[int] = iter(range(1, len(args))) | |
| for i in args_iterator: | |
| arg: str = args[i] | |
| skip_next: bool = False | |
| if arg in ('-h', '--help'): | |
| usage(args[0]) | |
| sys.exit(0) | |
| elif arg in ('-', '--stdin'): | |
| input_filename = '' | |
| elif arg in ('-c', '--stdout'): | |
| output_filename = '' | |
| elif arg in ('-o', '--output'): | |
| output_filename = i + 1 < len(args) and args[i + 1] or '' | |
| skip_next = True | |
| elif arg in ('-f', '--force'): | |
| force = True | |
| elif arg in ('-i', '--integer'): | |
| integer = i + 1 < len(args) and from_str(args[i + 1], int, integer) | |
| skip_next = True | |
| elif arg in ('-r', '--real'): | |
| real = i + 1 < len(args) and from_str(args[i + 1], float, real) | |
| skip_next = True | |
| elif arg.startswith('-'): | |
| print(f'Unknown option \'{arg}\'.') | |
| sys.exit(1) | |
| elif not input_filename: | |
| input_filename = arg | |
| else: | |
| positional.append(arg) | |
| skip_next and i + 1 < len(args) and next(args_iterator) | |
| return input_filename, output_filename, force, integer, real, positional | |
| def open_io(input_filename: str, output_filename: str, force: bool) -> Tuple[IO[str], IO[str]]: | |
| input_file: IO[str] | |
| output_file: IO[str] | |
| if input_filename: | |
| try: | |
| input_file = open(input_filename, 'r') | |
| except OSError: | |
| print(f'Cannot open input file \'{input_filename}\'.') | |
| sys.exit(1) | |
| else: | |
| input_file = sys.stdin | |
| if output_filename: | |
| if force or not os.path.exists(output_filename) or output_filename == '/dev/null': | |
| try: | |
| output_file = open(output_filename, 'w') | |
| except OSError: | |
| print(f'Cannot open output file \'{output_filename}\'.') | |
| sys.exit(1) | |
| else: | |
| print(f'Output file \'{output_filename}\' exists, pass \'-f\' to overwrite.') | |
| sys.exit(1) | |
| else: | |
| output_file = sys.stdout | |
| return input_file, output_file | |
| def close_io(input_file: IO[str], output_file: IO[str]) -> None: | |
| if input_file != sys.stdin: | |
| input_file.close() | |
| if output_file != sys.stdout: | |
| output_file.close() | |
| def copy_io(input_file: IO[str], output_file: IO[str]) -> None: | |
| output_file.write(input_file.read()) | |
| def main(args: List[str]) -> None: | |
| # Command line parsing. | |
| input_filename: str | |
| output_filename: str | |
| force: bool | |
| integer: int | |
| real: float | |
| positional: List[str] | |
| input_filename, output_filename, force, integer, real, positional = parse_command_line(args) | |
| # Simply copy input to output. | |
| input_file: IO[str] | |
| output_file: IO[str] | |
| input_file, output_file = open_io(input_filename, output_filename, force) | |
| copy_io(input_file, output_file) | |
| close_io(input_file, output_file) | |
| if __name__ == '__main__': | |
| sys.exit(main(sys.argv)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment