#!/bin/bash
# Copyright 2026 Andrew 'Diddymus' Rolfe
# Released under the MIT license.
# SPDX-License-Identifier: MIT
#
# `inc` is a simple script to recursively include files in other files. `inc`
# is file type agnostic and can be used for a number of tasks such as creating
# text or Markdown documents, configuration files, XML documents, JSON files
# etc. A simple example:
#
# text1.txt:
#   This is the start of the first file.
#   {inc: ./text2.txt}
#   This is the end of the first file.
#
# text2.txt
#   This is from file two.
#     {inc: ./text3.txt}
#
# text3.txt
#   From file three.
#
# This can be run as: `inc < text1.txt > text.txt`
#
# The resulting text.txt will contain:
#
#   This is the start of the first file.
#   This is from file two.
#     From file three.
#   This is the end of the first file.
#
# By default the include directive is `{inc: <file>}`. This can be changed by
# setting PREFIX and SUFFIX envvars. For example, PREFIX="[#INCLUDE" SUFFIX="]"
# will change the include directive to `[#INCLUDE <file>]`. You can also have
# very short directives. For example, PREFIX=":" SUFFIX=":" will change the
# include directive to `:file:`. However if any lines contain two colons `inc`
# may mistake the intervening text as a file name. Choose your PREFIX and
# SUFFIX wisely!
#
# Relative paths within include directives are evaluated relative to the file
# containing the directive.
#
# Included files will keep their indenting relative to the file they are being
# included in. An error will be thrown if an included file cannot be found.
# However, blank placeholders without a file, such as `{inc: }`, are allowed
# and will be skipped.
#
# For the top level file an initial indent may be specified via the INDENT
# envvar. Using the above example, `INDENT="> " inc < text1.txt > text.txt`
# produces:
#
#   > This is the start of the first file.
#   > This is from file two.
#   >   From file three.
#   > This is the end of the first file.
#
# Include directives must appear on their own line and may not be embedded in a
# line. Cyclic imports are detected.
#
# While `inc` will handle 500 nested includes easily it will start to slow
# down. While `inc` will handle traversing 500 directories deep it will start
# to slow down.  Feel free to get pathological, `inc` will slow down a bit and
# still handle it.

shopt -s extglob

PREFIX="${PREFIX:-"{inc:"}"
SUFFIX="${SUFFIX:-"}"}"
INDENT="${INDENT:-""}"

if [ "$1" == "--child" ]; then
  SEEN="${SEEN:-":"}"
  INC_BIN="$INC_BIN"
  INFILE="${INFILE:-"main file"}"

  if [ -n "$INC_DIR" ]; then
    cd "$INC_DIR" || exit 1
  fi
else
  SEEN=":"
  INC_BIN="$(realpath "$0")"

  if [ -n "$1" ] && [ -f "$1" ]; then
    INFILE="$1"
    exec < "$1"
  else
    INFILE="main file"
  fi
fi

while IFS= read -r line || [ -n "$line" ]; do
  if [[ "$line" != *"$PREFIX"*"$SUFFIX"* ]]; then
    printf '%s%s\n' "$INDENT" "$line"
    continue
  fi

  left="${line%%"$PREFIX"*}"
  rest="${line#*"$PREFIX"}"

  file="${rest%%"$SUFFIX"*}"
  right="${rest#*"$SUFFIX"}"

  file="${file#"${file%%[![:space:]]*}"}"
  file="${file%"${file##*[![:space:]]}"}"

  if [ -n "${left##*([[:space:]])}" ] || [ -n "${right##*([[:space:]])}" ]; then
    printf 'Error: "%s", inline includes not supported for "%s"\n' "$INFILE" "$file" >&2
    exit 1
  fi

  [ -n "$file" ] || continue;

  if [ ! -f "$file" ]; then
    printf 'Error: "%s", include file not found: "%s"\n' "$INFILE" "$file" >&2
    exit 1
  fi

  target_dir=$(dirname "$file")
  target_file=$(basename "$file")

  target_inode=$(ls -di "$target_dir")
  target_inode="${target_inode%% *}"

  if [ "${SEEN#*${target_inode}/${target_file}}" != "$SEEN" ]; then
    printf 'Error: "%s", include cycle detected for: "%s"\n' "$INFILE" "$file" >&2
    exit 1
  fi

  INC_DIR="$target_dir"                   \
  SEEN="$SEEN$target_inode/$target_file:" \
  INFILE="$file"                          \
  INDENT="$INDENT$left"                   \
  INC_BIN="$INC_BIN"                      \
  "$INC_BIN" "--child" < "$file" || exit 1
done
