52 lines
1.5 KiB
Python
52 lines
1.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Require PATH-resolved Bash in scripts, generated scripts, and examples.
|
|
|
|
Nix and other non-FHS environments need not provide Bash at a fixed bin path.
|
|
Scan tracked text, including extensionless scripts and embedded shell payloads.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
|
|
|
|
FIXED_BASH = re.compile(rb"#![ \t]*/(?:usr/)?bin/bash\b")
|
|
|
|
|
|
def check(root: Path) -> list[str]:
|
|
tracked = subprocess.run(
|
|
["git", "ls-files", "-z"], cwd=root, check=True, capture_output=True,
|
|
).stdout
|
|
findings = []
|
|
for name in tracked.split(b"\0"):
|
|
if not name:
|
|
continue
|
|
relative = name.decode("utf-8", errors="surrogateescape")
|
|
path = root / relative
|
|
if path.is_symlink() or not path.is_file():
|
|
continue
|
|
data = path.read_bytes()
|
|
if b"\0" in data:
|
|
continue
|
|
for number, line in enumerate(data.splitlines(), 1):
|
|
if FIXED_BASH.search(line):
|
|
findings.append(f"{relative}:{number}: use #!/usr/bin/env bash")
|
|
return findings
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
|
|
args = parser.parse_args()
|
|
findings = check(args.root)
|
|
for finding in findings:
|
|
print(finding)
|
|
print(f"Bash shebang check: {len(findings)} violation(s)")
|
|
return int(bool(findings))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|