"""CLI utility for formatting Tcl code.""" import argparse import pathlib import sys from tclint.cli.resolver import Resolver from tclint.cli.utils import register_codec_warning from tclint.commands.plugins import PluginManager from tclint.config import ( Config, ConfigError, SpacesInBraces, setup_tclfmt_config_cli_args, ) from tclint.format import Formatter, FormatterOpts from tclint.parser import Parser, TclSyntaxError try: from tclint._version import __version__ # type: ignore except ModuleNotFoundError: __version__ = "(unknown version)" # exit code flags EXIT_OK = 0 EXIT_FORMAT_VIOLATIONS = 1 EXIT_SYNTAX_ERROR = 2 EXIT_INPUT_ERROR = 4 def format( script: str, config: Config, plugins: PluginManager, debug=False, debug_whitespace=False, partial=False, ) -> str: parser = Parser(debug=debug, commands=plugins.get_commands(config.commands)) formatter = Formatter( FormatterOpts( indent=config.get_indent(), indent_mixed_tab_size=config.get_indent_mixed_tab_size(), spaces_in_braces=( config.style_spaces_in_braces == SpacesInBraces.ALWAYS or config.style_spaces_in_braces == SpacesInBraces.BALANCED_YES ), balanced_spaces_in_braces=( config.style_spaces_in_braces == SpacesInBraces.BALANCED_NO or config.style_spaces_in_braces == SpacesInBraces.BALANCED_YES ), max_blank_lines=config.style_max_blank_lines, indent_namespace_eval=config.style_indent_namespace_eval, emacs=config.style_emacs, debug_whitespace=debug_whitespace, ) ) if partial: return formatter.format_partial(script, parser) else: return formatter.format_top(script, parser) def check(path: str, script: str, formatted: str): parser = Parser() original_tree = parser.parse(script) formatted_tree = parser.parse(formatted) if original_tree != formatted_tree: print(f"Warning: {path} syntax trees don't match", file=sys.stderr) print("\n".join(original_tree.diff(formatted_tree)), file=sys.stderr) def main(): parser = argparse.ArgumentParser("tclfmt") parser.add_argument( "--version", action="version", version=f"%(prog)s {__version__}" ) parser.add_argument( "source", nargs="+", help=( "files to format. By default, prints formatted files to stdout. Provide '-'" " to read from stdin" ), type=pathlib.Path, ) mode_group = parser.add_argument_group("mode") mode_mutex = mode_group.add_mutually_exclusive_group(required=False) mode_mutex.add_argument( "--in-place", help="update files that require formatting", action="store_true" ) mode_mutex.add_argument( "--check", help="list files that require formatting and set the exit code", action="store_true", ) parser.add_argument( "-d", "--debug", action="count", default=0, help=( "display debug output. Provide additional times to increase the verbosity" " of output (e.g. -dd)" ), ) parser.add_argument( "--debug-whitespace", action="store_true", default=False, help="display whitespace in debug mode.", ) parser.add_argument( "-c", "--config", help="path to config file", type=pathlib.Path, default=None, metavar="", ) parser.add_argument( "--partial", help="treat input as a fragment of a script", action="store_true", ) cwd = pathlib.Path.cwd() setup_tclfmt_config_cli_args(parser, cwd) args = parser.parse_args() global_config = None if args.config is not None: try: global_config = Config.from_path(args.config, cwd) global_config.apply_cli_args(args) except FileNotFoundError: print(f"Config file path doesn't exist: {args.config}") return EXIT_INPUT_ERROR except ConfigError as e: print(f"Invalid config file: {e}") return EXIT_INPUT_ERROR resolver = Resolver(args, global_config) try: sources = resolver.resolve_sources(args.source, cwd) except FileNotFoundError as e: print(f"Invalid path provided: {e}") return EXIT_INPUT_ERROR except ConfigError as e: print(f"Invalid config file: {e}") return EXIT_INPUT_ERROR plugin_manager = PluginManager(trust_uninstalled=args.trust_plugins) retcode = EXIT_OK register_codec_warning("replace_with_warning") reformat_count = 0 for path, config in sources: if path is None: script = sys.stdin.read() out_prefix = "(stdin)" else: with open(path, "r", errors="replace_with_warning") as f: script = f.read() out_prefix = str(path) try: formatted = format( script, config, plugin_manager, debug=(args.debug > 1), debug_whitespace=args.debug_whitespace, partial=args.partial, ) if args.in_place and path: with open(path, "w") as f: f.write(formatted) elif args.check: if script != formatted: print(f"{out_prefix}: needs reformatting") retcode |= EXIT_FORMAT_VIOLATIONS reformat_count += 1 else: if args.in_place: print("Warning: --in-place option ignored when reading from stdin") print(formatted, end="") if args.debug > 0: if args.debug_whitespace: print( "Warning: --debug-whitespace enabled, disabling original vs." " formatted syntax tree check" ) else: check(out_prefix, script, formatted) except TclSyntaxError as e: line, col = e.start print(f"{out_prefix}:{line}:{col}: syntax error: {e}", file=sys.stderr) retcode |= EXIT_SYNTAX_ERROR continue if args.check: messages = [] if reformat_count == 0: messages.append("Formatting clean!") elif reformat_count == 1: messages.append("1 file needs reformatting.") else: messages.append(f"{reformat_count} files need reformatting.") messages.append( f"Checked {len(sources)} file{'s' if len(sources) != 1 else ''}." ) print(" ".join(messages)) return retcode if __name__ == "__main__": sys.exit(main())