from __future__ import annotations import argparse import json import logging import shutil import subprocess import sys import time from enum import Enum from typing import NamedTuple LINTER_CODE = "SHELLCHECK" class LintSeverity(str, Enum): ERROR = "error" WARNING = "warning" ADVICE = "advice" DISABLED = "disabled" class LintMessage(NamedTuple): path: str | None line: int | None char: int | None code: str severity: LintSeverity name: str original: str | None replacement: str | None description: str | None def run_command( args: list[str], ) -> subprocess.CompletedProcess[bytes]: logging.debug("$ %s", " ".join(args)) start_time = time.monotonic() try: return subprocess.run( args, capture_output=True, ) finally: end_time = time.monotonic() logging.debug("took %dms", (end_time - start_time) * 1000) def check_files( files: list[str], ) -> list[LintMessage]: try: proc = run_command( ["shellcheck", "--external-sources", "--format=json1"] + files ) except OSError as err: return [ LintMessage( path=None, line=None, char=None, code=LINTER_CODE, severity=LintSeverity.ERROR, name="command-failed", original=None, replacement=None, description=(f"Failed due to {err.__class__.__name__}:\n{err}"), ) ] stdout = str(proc.stdout, "utf-8").strip() results = json.loads(stdout)["comments"] return [ LintMessage( path=result["file"], name=f"SC{result['code']}", description=result["message"], line=result["line"], char=result["column"], code=LINTER_CODE, severity=LintSeverity.ERROR, original=None, replacement=None, ) for result in results ] if __name__ == "__main__": parser = argparse.ArgumentParser( description="shellcheck runner", fromfile_prefix_chars="@", ) parser.add_argument( "filenames", nargs="+", help="paths to lint", ) if shutil.which("shellcheck") is None: err_msg = LintMessage( path="", line=None, char=None, code=LINTER_CODE, severity=LintSeverity.ERROR, name="command-failed", original=None, replacement=None, description="shellcheck is not installed, did you forget to run `lintrunner init`?", ) print(json.dumps(err_msg._asdict()), flush=True) sys.exit(0) args = parser.parse_args() lint_messages = check_files(args.filenames) for lint_message in lint_messages: print(json.dumps(lint_message._asdict()), flush=True)