#!/bin/bash

set -o pipefail

if (( $# < 1 )); then
cat <<'EOF'
Usage: sectmod SOURCE [PARAM ...] 
Parameters can be passed to scripts inside by $$1, $$2, etc.
 $$1 - contained source file (first parameter), etc.
 $$$ - will be replaced with the name of the temp-file,
 containing the original data of the section that will be replaced.
Try --help for more.
EOF
    exit 2
fi

case "$1" in
    -h|--help)
cat <<'EOF'
Конструкция "<<<!", идентифицирует "врезку" sectmod.
Если эта конструкция не встречается в файле, то никаких изменений sectmod не вносит,
то есть, на выходе будет получены те же данные, что были на входе.

Для использования "<<<!" есть два базовых варианта синтаксиса:

1. Многострочная секция:
 имя_секции <<<! командная строка
 содержимое секции  (заменяется на результат выполнения командной строки)
 /имя_секции

2. Однострочный режим:
 однострочный режим - это когда "секция" состоит только из одной строки.
 этот случай определяется когда в строке есть также закрывающая конструкция ">>>".
 Выглядит это примерно так:
   какой-то текст <<<! командная строка >>> ещё текст
 в этом случае результат будет вставлен в строку вместо конструкции <<<! ... >>>

В каждом режиме предусмотрены дополнительные варианты синтаксиса,
которые отличаются количеством знаков "!", то есть "<<<!", "<<<!!", "<<<!!!", и т.д.
Вариация "<<<!!!" заменяет всё содержимое секции на результат выводимой командой.

Подробнее о вариациях
в многострочном режиме:
"<<<!" - выводит полностью строки заголовка секции, вместе с командой
"<<<!!" - выводит строки заголовка секции, БЕЗ команды (т.е.только левую часть)
"<<<!!!" - не выводит строки заголовка секции, выводит только результат
"<<<!!!!" - то же что предыдущий, но в файл данных секции
            первой строкой добавляется строка параметров вызова.
в однострочном режиме:
abc<<<! echo 123 >>>def  -- будет "# abc123def" (результат вставится между)
abc<<<!! echo 123 >>>def -- будет выведена без изменений исходная строка, выполнится команда
abc<<<!!! echo 123 >>> -- будет "123", строка будет заменена на результат выполнения команды
abc<<<!!!! echo 123 >>>-- указанная команда выполнится в текущем окружении (вывод выведется)

Выполнение команд в текущем окружении конструкцией <<<!!!! ... >>>
даёт возможность взаимодействия с переменными
это можно использовать, например, для присвоения переменных и дальнейшей их подстановки.
Возможно влияние на ход компиляции sectmod:
<<<!!!! stopsect=1 >>> выключает компиляцию многострочных секций (0-включает обратно)
   это может быть полезно когда требуется "закомментировать" некоторый участок от sectmod.
<<<!!!! in_section=1 >>> полностью выключает компиляции секций sectmod
   и выводит дальнейшее содержимое файла "как есть", не выполняя никаких подстановок.
EOF
        exit 0
        ;;
esac


source_file="$1"

# Все параметры будут доступны командам как
# $$0, $$1, ... и т.д.
params=("$0" "$@")

if [[ ! -f "$source_file" ]]; then
    echo "sectmod: file not found: $source_file" >&2
    exit 1
fi

# имя временного файла (по умолчанию пустое)
temp_file=

cleanup()
{
    if [[ -n "$temp_file" ]]; then
        rm -f -- "$temp_file"
    fi
}

# При нормальном завершении выполняется cleanup.
trap cleanup EXIT

# При сигналах сначала завершаем shell, после чего сработает EXIT trap.
trap 'exit 130' INT TERM HUP

process()
{
    local line
    local left
    local right
    local name
    local command
    local close_line
    local -a section_lines
    local in_section=0
    local bang_count
    local tmp
    local ret
    local line_num=0
    local i
    local q
    local stopsect=0

    # Заранее собираем параметры в эту переменную
    local paramsum=""
    for (( i=1; i<${#params[@]} && i<10; i++ )); do
        printf -v q '%q' "${params[$i]}"
        paramsum+="$q "
    done
    paramsum="${paramsum% }"

    while IFS= read -r line || [[ -n "$line" ]]; do
        ((line_num++))

        # Ищем начало секции, предназначенной для замены.
        if (( ! in_section )) && [[ "$line" == *'<<<!'* ]]; then

            # Всё после "<<<!" — команда.
            command="${line#*<<<!}"

            # Всё, что находится слева от "<<<!".
            left="${line%%<<<!*}"

            # проверяем, есть ли в $line ">>>"
            if [[ "$command" == *'>>>'* ]]; then
                # если есть, то это не секционный, а однострочный режим
                in_section=0
                # Всё что находится лева от >>>
                command="${command%%>>>*}"
                # всё что после >>> это продолжение строки
                right="${line#*>>>}"
            else
                if (( stopsect )); then
                    # если заблокирован режим многострочных секций
                    printf '%s\n' "$line"
                    continue
                fi
                # режим секционный (многострочный)
                # Проверяем, что есть хотя бы 1 пробел справа в left.
                # Если пробела нет, то это не маркер секции — выводим строку как есть.
                if [[ "$left" != *[[:space:]] ]]; then
                    printf '%s\n' "$line"
                    continue
                fi

                # Убираем пробелы справа.
                left="${left%"${left##*[![:space:]]}"}"

                # Имя секции — последнее слово слева.
                # name="${left##*([[:space:]])}"
                # name="${name##* }"
                # name="${name##*$'\t'}"
                name="${left##*[[:space:]]}"

                # очищаем name от спецсимволов слева и справа
                while [[ $name && ${name:0:1} != [a-zA-Z0-9_] ]]; do
                    name=${name:1}
                done

                while [[ $name && ${name: -1} != [a-zA-Z0-9_] ]]; do
                    name=${name:0:${#name}-1}
                done

                if [[ -z "$name" ]]; then
                    echo "sectmod: cannot determine section name:" >&2
                    echo "    $line" >&2
                    return 1
                fi
                # запоминаем номер линии начала секции, от 1 и более
                (( in_section=line_num ))
            fi

            # Подсчитаем количество знаков "!"
            # в начале command в bang_count и отрежем их.
            bang_count=0
            tmp="$command"

            while [[ "$tmp" == '!'* ]]; do
                (( bang_count++ ))
                tmp="${tmp#!}"
            done
            command="${command:$bang_count}"

            # Убираем пробелы в начале команды.
            command="${command#"${command%%[![:space:]]*}"}"

            # Заменяем в $command все значения вида $$n (где n-цифра от 0 до 9) на
            # значения параметров, переданных скрипту, соответственно $1, $2, и т.д.
            for (( i=0; i<${#params[@]} && i<10; i++ )); do
                printf -v q '%q' "${params[$i]}"
                command="${command//\$\$${i}/$q}"
            done

            if [[ "$command" == *'$$N'* ]]; then
                # заменяем $$N на номер текущей строки
                command="${command//\$\$N/$line_num}"
            fi

            section_lines=()

            if (( in_section )); then
                # Разветвление по количеству bang_count.
                if (( bang_count == 0 )); then
                    # Выводим открывающую строку без изменений.
                    printf '%s\n' "$line"
                elif (( bang_count == 1 )); then
                    # Выводим только значение "$left".
                    printf '%s\n' "$left"
                fi
                # bang_count > 1 — не выводим эту строку вообще.

                close_line=""

                if (( bang_count == 3 )); then
                    # для случая с четырьмя восклицательными знаками
                    # добавляем строку-заголовок с важными параметрами
                    section_lines+=( "$line_num $paramsum" )
                    # добавляем открывающую линию
                    section_lines+=( "$line" )
                fi

                # Ищем закрывающую строку.
                while IFS= read -r line || [[ -n "$line" ]]; do
                    ((line_num++))

                    if [[ "$line" == *"/$name"* ]]; then
                        close_line="$line"
                        break
                    fi

                    section_lines+=( "$line" )
                done

                # EOF до закрывающей строки.
                if [[ -z "$close_line" ]]; then
                    echo "sectmod: section $in_section:'$name' has no closing '/$name'" >&2
                    return 1
                fi

                if (( bang_count == 3)); then
                    section_lines+=( "$close_line" )
                fi
            fi

            if [[ "$command" == *'$$$'* ]]; then
                # Если команда содержит $$$, помещаем исходное содержимое
                # секции во временный файл (либо данные строки, если однострочный режим)

                if [[ -z "$temp_file" ]]; then
                    temp_file=$(mktemp) || {
                        echo "sectmod: cannot create temporary file" >&2
                        return 1
                    }
                fi

                # Заменяем все $$$ на имя временного файла.
                command="${command//\$\$\$/$temp_file}"

                if (( ! in_section )); then
                    # в однострочном режиме всегда передаём данные об исходной строке
                    # (в этом режиме одна строка это как бы и есть вся секция)
                    section_lines+=( "$line_num $paramsum")
                    section_lines+=( "$line" )
                    section_lines+=( "$left" )
                    section_lines+=( "$right" )
                    # Добавляем в конце компилированную команду
                    section_lines+=( "$command" )
                fi
                
                # Пустая секция должна дать действительно пустой файл.
                if (( ${#section_lines[@]} )); then
                    printf '%s\n' "${section_lines[@]}" > "$temp_file" || {
                        echo "sectmod: cannot write temporary file: $temp_file" >&2
                        return 1
                    }
                else
                    : > "$temp_file" || {
                        echo "sectmod: cannot write temporary file: $temp_file" >&2
                        return 1
                    }
                fi
            fi

            if (( in_section )); then
                # Выполняем команду (многострочный режим)
                # вывод сразу попадает в вывод stdout
                if bash -c "$command"; then
                    :
                else
                    ret=$?
                    echo "sectmod: command failed for section $in_section-$line_num:'$name' (exit code: $ret)" >&2
                    return "$ret"
                fi
            else
                # Выполняем команду в однострочном режиме
                if (( bang_count > 2 )); then
                    eval "$command"
                    ret=$?

                    if (( ret != 0 )); then
                        echo "sectmod: local in-line command failed $line_num:'$command' (exit code: $ret)" >&2
                        return "$ret"
                    fi
                    continue
                else
                    if close_line=$(bash -c "$command"); then
                        # Полученный результат выводим в зависимости от bang_count.
                        if (( bang_count == 0 )); then
                            # Собираем результат в одну строку
                            close_line="$left$close_line$right"
                        elif (( bang_count == 1 )); then
                            # для двух ! выводим всю исходную строку вместо результата
                            close_line="$line"
                        elif (( bang_count == 2 )); then
                            # для трёх ! выводим только результат
                            printf '%s\n' "$close_line"
                        fi
                    else
                        echo "sectmod: command failed for line $line_num:'$line'" >&2
                        return 1
                    fi
                fi
            fi

            in_section=0

            if (( bang_count < 2 )); then
                # Закрывающая строка остаётся без изменений.
                printf '%s\n' "$close_line"
            fi

            continue
        fi

        printf '%s\n' "$line"

    done < "$source_file"
}

process
