summaryrefslogtreecommitdiffstats
path: root/tools/lint/shell/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/lint/shell/__init__.py')
-rw-r--r--tools/lint/shell/__init__.py148
1 files changed, 148 insertions, 0 deletions
diff --git a/tools/lint/shell/__init__.py b/tools/lint/shell/__init__.py
new file mode 100644
index 0000000000..b75dc2d159
--- /dev/null
+++ b/tools/lint/shell/__init__.py
@@ -0,0 +1,148 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import json
+import os
+from json.decoder import JSONDecodeError
+
+import mozpack.path as mozpath
+from mozfile import which
+from mozlint import result
+from mozlint.util.implementation import LintProcess
+from mozpack.files import FileFinder
+
+SHELLCHECK_NOT_FOUND = """
+Unable to locate shellcheck, please ensure it is installed and in
+your PATH or set the SHELLCHECK environment variable.
+
+https://shellcheck.net or your system's package manager.
+""".strip()
+
+results = []
+
+
+class ShellcheckProcess(LintProcess):
+ def process_line(self, line):
+ try:
+ data = json.loads(line)
+ except JSONDecodeError as e:
+ print("Unable to load shellcheck output ({}): {}".format(e, line))
+ return
+
+ for entry in data:
+ res = {
+ "path": entry["file"],
+ "message": entry["message"],
+ "level": "error",
+ "lineno": entry["line"],
+ "column": entry["column"],
+ "rule": entry["code"],
+ }
+ results.append(result.from_config(self.config, **res))
+
+
+def determine_shell_from_script(path):
+ """Returns a string identifying the shell used.
+
+ Returns None if not identifiable.
+
+ Copes with the following styles:
+ #!bash
+ #!/bin/bash
+ #!/usr/bin/env bash
+ """
+ with open(path, "r") as f:
+ head = f.readline()
+
+ if not head.startswith("#!"):
+ return
+
+ # allow for parameters to the shell
+ shebang = head.split()[0]
+
+ # if the first entry is a variant of /usr/bin/env
+ if "env" in shebang:
+ shebang = head.split()[1]
+
+ if shebang.endswith("sh"):
+ # Strip first to avoid issues with #!bash
+ return shebang.strip("#!").split("/")[-1]
+ # make it clear we return None, rather than fall through.
+ return
+
+
+def find_shell_scripts(config, paths):
+ found = dict()
+
+ root = config["root"]
+ exclude = [mozpath.join(root, e) for e in config.get("exclude", [])]
+
+ if config.get("extensions"):
+ pattern = "**/*.{}".format(config.get("extensions")[0])
+ else:
+ pattern = "**/*.sh"
+
+ files = []
+ for path in paths:
+ path = mozpath.normsep(path)
+ ignore = [
+ e[len(path) :].lstrip("/")
+ for e in exclude
+ if mozpath.commonprefix((path, e)) == path
+ ]
+ finder = FileFinder(path, ignore=ignore)
+ files.extend([os.path.join(path, p) for p, f in finder.find(pattern)])
+
+ for filename in files:
+ shell = determine_shell_from_script(filename)
+ if shell:
+ found[filename] = shell
+ return found
+
+
+def run_process(config, cmd):
+ proc = ShellcheckProcess(config, cmd)
+ proc.run()
+ try:
+ proc.wait()
+ except KeyboardInterrupt:
+ proc.kill()
+
+
+def get_shellcheck_binary():
+ """
+ Returns the path of the first shellcheck binary available
+ if not found returns None
+ """
+ binary = os.environ.get("SHELLCHECK")
+ if binary:
+ return binary
+
+ return which("shellcheck")
+
+
+def lint(paths, config, **lintargs):
+ log = lintargs["log"]
+ binary = get_shellcheck_binary()
+
+ if not binary:
+ print(SHELLCHECK_NOT_FOUND)
+ if "MOZ_AUTOMATION" in os.environ:
+ return 1
+ return []
+
+ config["root"] = lintargs["root"]
+
+ files = find_shell_scripts(config, paths)
+
+ base_command = [binary, "-f", "json"]
+ if config.get("excludecodes"):
+ base_command.extend(["-e", ",".join(config.get("excludecodes"))])
+
+ for f in files:
+ cmd = list(base_command)
+ cmd.extend(["-s", files[f], f])
+ log.debug("Command: {}".format(cmd))
+ run_process(config, cmd)
+ return results