Created
June 9, 2026 14:13
-
-
Save c0m1c5an5/e3c43b6a01e404003637603d5a1ab1f0 to your computer and use it in GitHub Desktop.
Python rewrite of carlocorradini/inline
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """Inline bash `source` / `.` directives into a single script.""" | |
| import argparse | |
| import logging | |
| import re | |
| import shutil | |
| import sys | |
| from pathlib import Path | |
| from typing import Optional | |
| log = logging.getLogger("inline") | |
| def setup_logging(verbosity: int) -> None: | |
| if verbosity == 0: | |
| root_level = logging.WARNING | |
| tool_level = logging.WARNING | |
| elif verbosity == 1: | |
| root_level = logging.INFO | |
| tool_level = logging.INFO | |
| elif verbosity == 2: | |
| root_level = logging.INFO | |
| tool_level = logging.DEBUG | |
| else: | |
| root_level = logging.DEBUG | |
| tool_level = logging.DEBUG | |
| logging.root.setLevel(root_level) | |
| logging.getLogger("inline").setLevel(tool_level) | |
| fmt = logging.Formatter("%(asctime)s %(levelname)-8s %(message)-100s [%(filename)s:%(lineno)d]") | |
| hdlr = logging.StreamHandler() | |
| hdlr.setFormatter(fmt) | |
| logging.root.addHandler(hdlr) | |
| def parse_inline_skip(line: str) -> bool: | |
| return bool(re.search(r"#\s*inline\s+skip", line)) | |
| def parse_shebang(line: str) -> bool: | |
| return line.startswith("#!") | |
| def parse_source(line: str) -> Optional[str]: | |
| m = re.match(r"""^(?:\s*)(?:source|\.)\s+["']?([^"'\s]+)["']?""", line) | |
| return m.group(1) if m else None | |
| def parse_shellcheck_source(line: str) -> Optional[str]: | |
| m = re.match(r'^\s*#\s*shellcheck\s+source="?([^"\s]+)"?', line) | |
| return m.group(1) if m else None | |
| def _resolve_source(ref: str, file_dir: Path, shellcheck_hint: Optional[str]) -> Optional[Path]: | |
| if ref.startswith("$") and "/" in ref: | |
| ref = str(file_dir / ref.split("/", 1)[1]) | |
| elif "/" not in ref: | |
| log.debug("Searching $PATH for '%s'", ref) | |
| found = shutil.which(ref) | |
| if found: | |
| return Path(found) | |
| if not ref.startswith("/"): | |
| ref = str(file_dir / ref) | |
| path = Path(ref).expanduser().resolve() | |
| log.debug("Path candidate '%s'", path) | |
| if path.is_file(): | |
| return path | |
| if shellcheck_hint: | |
| log.debug("'%s' not found, trying shellcheck hint", path) | |
| hint = shellcheck_hint if shellcheck_hint.startswith("/") else str(file_dir / shellcheck_hint) | |
| hint_path = Path(hint).resolve() | |
| log.debug("Path candidate '%s'", hint_path) | |
| if hint_path.is_file(): | |
| return hint_path | |
| return None | |
| def _inline(file: Path, visited: set) -> list[str]: | |
| file = file.resolve() | |
| log.info("Reading '%s'", file) | |
| visited.add(file) | |
| out: list[str] = [] | |
| prev_line: Optional[str] = None | |
| with file.open() as lines: | |
| for line in lines: | |
| line = line.rstrip("\n") | |
| log.debug("Analyzing line '%s'", line) | |
| if parse_shellcheck_source(line): | |
| out.append(line) | |
| elif ref := parse_source(line): | |
| if parse_inline_skip(line): | |
| log.warning("Skipping source '%s'", line) | |
| out.append(line) | |
| else: | |
| hint = parse_shellcheck_source(prev_line) if prev_line else None | |
| path = _resolve_source(ref, file.parent, hint) | |
| if path is None: | |
| log.error("Unable to resolve source file path '%s'", ref) | |
| sys.exit(1) | |
| log.debug("'%s' resolved to '%s'", ref, path) | |
| out.append(f"# {line}") | |
| if path in visited: | |
| log.error("Recursion detected: '%s' sourced from '%s'", ref, file) | |
| sys.exit(1) | |
| out.append(f"# -----BEGIN {path}-----") | |
| out.extend(l for l in _inline(path, visited) if not parse_shebang(l)) | |
| out.append(f"# -----END {path}-----") | |
| else: | |
| out.append(line) | |
| prev_line = line | |
| return out | |
| def inline(file: Path) -> list: | |
| return _inline(file.resolve(), set()) | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Inline bash source directives into a single script.") | |
| parser.add_argument("in_file", help="Input script") | |
| parser.add_argument("--out-file", help="Output file") | |
| parser.add_argument("--overwrite", action="store_true", help="Overwrite the input file") | |
| parser.add_argument( | |
| "-v", "--verbose", action="count", default=0, help="Increase verbosity (repeatable up to 3 times)." | |
| ) | |
| args = parser.parse_args() | |
| setup_logging(verbosity=args.verbose) | |
| in_file = Path(args.in_file) | |
| if not in_file.is_file(): | |
| log.error("Input file '%s' does not exist", in_file) | |
| sys.exit(1) | |
| if args.overwrite: | |
| out_file = in_file | |
| elif args.out_file: | |
| out_file = Path(args.out_file) | |
| else: | |
| out_file = in_file.with_stem(in_file.stem + ".inlined") | |
| if not args.overwrite and out_file.exists(): | |
| log.error("Output file '%s' already exists", out_file) | |
| sys.exit(1) | |
| log.info("Inlining '%s'", in_file) | |
| result = inline(in_file) | |
| log.info("Writing '%s'", out_file) | |
| out_file.write_text("\n".join(result) + "\n") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment