225 lines
6.8 KiB
Python
225 lines
6.8 KiB
Python
"""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="<path>",
|
|
)
|
|
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())
|