|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +print_help() { |
| 5 | + cat <<'EOF' |
| 6 | +expand-includes.sh — simple textual include expander |
| 7 | +
|
| 8 | +USAGE: |
| 9 | + expand-includes.sh <input-file> |
| 10 | + expand-includes.sh -h | --help |
| 11 | +
|
| 12 | +DESCRIPTION: |
| 13 | + Reads <input-file> and writes the expanded result to stdout. |
| 14 | +
|
| 15 | + Lines matching the following form are replaced by the contents |
| 16 | + of the referenced file: |
| 17 | +
|
| 18 | + { include "path/to/file.yml.in" } |
| 19 | +
|
| 20 | + Rules: |
| 21 | + - The filepath MUST be in double quotes. |
| 22 | + - Leading whitespace (spaces and/or tabs) is preserved and applied |
| 23 | + to every line of the included file. |
| 24 | + - Trailing comments starting with '#' are ignored. |
| 25 | + - Whitespace around 'include' and the filename is ignored. |
| 26 | + - Included files may themselves contain { include "..." } directives. |
| 27 | + - Expansion is purely textual; no YAML parsing is performed. |
| 28 | +
|
| 29 | +EXAMPLE: |
| 30 | +
|
| 31 | + input.yml.in: |
| 32 | + jobs: |
| 33 | + build: |
| 34 | + runs-on: ubuntu-latest |
| 35 | + { include "common.yml.in" } # shared boilerplate |
| 36 | + steps: |
| 37 | + - run: echo done |
| 38 | +
|
| 39 | + common.yml.in: |
| 40 | + env: |
| 41 | + FOO: bar |
| 42 | + { include "more.yml.in" } |
| 43 | +
|
| 44 | + more.yml.in: |
| 45 | + timeout-minutes: 10 |
| 46 | +
|
| 47 | + Command: |
| 48 | + ./expand-includes.sh input.yml.in > output.yml |
| 49 | +
|
| 50 | + Result: |
| 51 | + jobs: |
| 52 | + build: |
| 53 | + runs-on: ubuntu-latest |
| 54 | + env: |
| 55 | + FOO: bar |
| 56 | + timeout-minutes: 10 |
| 57 | + steps: |
| 58 | + - run: echo done |
| 59 | +
|
| 60 | +NOTES: |
| 61 | + - This tool intentionally does not attempt to validate YAML. |
| 62 | + - Circular includes are not detected. |
| 63 | + - Designed for small, controlled include-based refactoring. |
| 64 | +
|
| 65 | +EOF |
| 66 | +} |
| 67 | + |
| 68 | +expand_file() { |
| 69 | + local file="$1" |
| 70 | + |
| 71 | + while IFS= read -r line || [[ -n "$line" ]]; do |
| 72 | + # Remove trailing comments for matching (keep original for output) |
| 73 | + local stripped="${line%%#*}" |
| 74 | + |
| 75 | + # Match YAML include line: |
| 76 | + # |
| 77 | + # <indent>include: "path" |
| 78 | + # |
| 79 | + if [[ "$stripped" =~ ^([[:space:]]*)include[[:space:]]*:[[:space:]]*\"([^\"]+)\"[[:space:]]*$ ]]; then |
| 80 | + local indent="${BASH_REMATCH[1]}" |
| 81 | + local include_file="${BASH_REMATCH[2]}" |
| 82 | + |
| 83 | + if [[ ! -f "$include_file" ]]; then |
| 84 | + echo "ERROR: included file not found: $include_file" >&2 |
| 85 | + return 1 |
| 86 | + fi |
| 87 | + |
| 88 | + # Recursively expand included file (no subshells) |
| 89 | + if ! mapfile -t inc_lines < <(expand_file "$include_file"); then |
| 90 | + return 1 |
| 91 | + fi |
| 92 | + |
| 93 | + for inc_line in "${inc_lines[@]}"; do |
| 94 | + printf "%s%s\n" "$indent" "$inc_line" |
| 95 | + done |
| 96 | + |
| 97 | + else |
| 98 | + echo "$line" |
| 99 | + fi |
| 100 | + done < "$file" |
| 101 | +} |
| 102 | + |
| 103 | + |
| 104 | +# ---- main ---- |
| 105 | + |
| 106 | +if [[ $# -ne 1 ]]; then |
| 107 | + print_help >&2 |
| 108 | + exit 1 |
| 109 | +fi |
| 110 | + |
| 111 | +case "$1" in |
| 112 | + -h|--help) |
| 113 | + print_help |
| 114 | + exit 0 |
| 115 | + ;; |
| 116 | +esac |
| 117 | + |
| 118 | +printf "# Warning: file generated by '%s %s', do not edit\n" "$0" "$*" |
| 119 | + |
| 120 | +expand_file "$1" |
0 commit comments