#!/bin/bash

# sectget - text section extractor by patterns matching

# help-rus
# Скрипт находит произвольную секцию текста через поиск её начала и конца.
# Границы секции можно уточнять постепенно, шаг за шагом сужая область выборки.
# На входе: имя файла (либо "-" для обработки stdin)
# далее в параметрах задаётся список пар "needle action",
# где needle - это строка для поиска (возможно с экранированными символами),
# action - символы действия.
# в action должен присутствовать символ "[" либо "]", что означает следующее:
#  "[" - означает, что найденная подстрока становится началом секции
#  "]" - означает, что найденная подстрока становится концом секции
# таким образом, каждая следующая пара "needle action" уменьшает размер секции,
# и в конце выводится то содержимое секции, какое останется после всех действий.
# Дополнительный action (исключение):
#  "@" - означает, что если подстрока в секции НЕ найдена, то поиск секции будет
#        начат заново (то есть пары "needle action" будут заново применяться к данным,
#        которые находятся далее - после "отвергнутой" секции до конца данных).
#       Если подстрока найдена, то исполнение "needle action" продолжается.
#       При этом возможны следующие уточняющие символы:
#       "+" - означает, что правый край секции будет расширен до конца данных.
#       Если "+" отсутствует, то областью дальнейших действий будет найденная секция.
#       "-" - означает, что все остальные данные исходного файла будут отброшены.
#        То есть, в этом случае происходит замена всего документа на найденную секцию.
#       "." - к "@" можно врменно добавлять для отладки.
#        В этом случае найденные и пропущенные секции будут дампиться для наглядности.
# Дополнительные символы действий:
#  "=" - присутствие этого символа отменяет экранирование символов в needle.
#  "+" - означает, что найденная подстрока будет включена в секцию (по умолчанию)
#  "-" - означает, что найденная подстрока будет исключена из секции.
#  "?" - означает, что подстрока опциональна, и если она не будет найдена,
#        то обработка будет продолжена и не повлияет на размер секции.
#  "L" - означает поиск последнего вхождения подстроки (по умолчанию - первого)
# 
# Особый случай (для упрощения наиболее частого сценария извлечения секции по имени):
# если количество аргументов после имени файла нечётное, то первый аргумент
# после имени файла считается именем секции по принципу "section ... /section".
# дальнейшие аргументы рассматриваются как обычные пары "needle action".
# 
# Особые опции:
#  Эти опции взаимоисключающие, то есть можно использовать только одну из них.
#  Опция должна ставиться самым первым аргументом (до имени исхоного файла):
#  --pos - выведет только номера символов начала и конца секции (через разделитель пробел).
#  --before - выведет только ту часть исходных данных, которая идёт до начала секции.
#  --after - выведет только ту часть исходных данных, которая идёт после конца секции.
#  --replace <file> - заменяет найденную секцию на содержимое указанного файла
#      (либо на данные из stdin, если указано "-"). 
# /help-rus

if [[ $# -eq 0 ]]; then
    echo "Try '--help' for more information."
cat <<'EOF'
Usage: sectget <file> <needle1> <action1> [<needle2> <action2> ...]
EOF
    exit 0
fi

pos_only=0
before_only=0
after_only=0
replace_only=0

case "$1" in
    --help|-h)
        echo "Russian:"
        "$0" "--1" "$0" "help-rus"
        exit
        ;;
    # обработка возможной опции --pos, она должна быть самой первой, если есть
    --pos)
        pos_only=1
        shift
        ;;
    --before)
        before_only=1
        shift
        ;;
    --after)
        after_only=1
        shift
        ;;
    --replace)
        replace_only=1
        shift
        # Загружаем данные, на которые заменяем найденную секцию. Если файл указан как "-", то читаем из stdin.
        replace_data="$1"
        shift
        if [[ "$replace_data" == "-" ]]; then
            replace_data=$(cat)
        elif [[ "$replace_data" == "" ]]; then
            # Если replace_data пустая строка, то заменяем найденную секцию на пустую строку.
            replace_data=""
        else
            if [[ ! -f "$replace_data" ]]; then
                echo "Error: replace file '$replace_data' does not exist." >&2
                exit 1
            fi
            replace_data=$(<"$replace_data")
        fi
        ;;
esac

strip_prefix=0
# Parse --<N> flag to strip N leading characters from each output line
if [[ "$1" =~ ^--[0-9]+$ ]]; then
    strip_prefix="${1#--}"
    shift
fi



file="$1"
shift

# Parse --<N> flag to strip N leading characters from each output line
if [[ "$1" =~ ^--[0-9]+$ ]]; then
    strip_prefix="${1#--}"
    shift
fi

# Добавление ++XXX к каждой строке, если первый аргумент начинается с "++"
if [[ "$1" == ++* ]]; then
    add_prefix="${1:2}"
    shift
else
    add_prefix=""
fi


# Проверить, является ли количество оставшихся аргументов чётным (каждая пара needle/action)
if (( $# % 2 != 0 )); then
    # Если количество аргументов нечётное, это не ошибка, а особый случай.
    # В этом случае мы предполагаем, что первый аргумент после файла — имя секции.
    # Если в имени секции отсутствует знак "/", то нормализуем секцию так:
    #   section -> section//section
    # После этого в section всегда присутствует знак "/".
    # Разделяем по этому знаку и получаем:
    #   opener = то, что до знака "/"
    #   closer = то, что после знака "/"
    # а затем преобразуем их в пары needle/action:
    #   opener "[-" и "closer" "]-"
    # Остальные аргументы оставляем без изменений после этих двух пар.
    section="$1"
    shift

    if [[ "$section" != */* ]]; then
        # если название секции не содержит "/", начинается с "[" и заканчивается на "]",
        # это значит что мы должны отрезать скобки по краям и использовать имя изнутри
        if [[ "$section" == \[*\] ]]; then
            section="${section:1:-1}"
        fi
        section="$section//$section"
    fi

    opener=${section%%/*}
    closer=${section#*/}

    set -- "$opener" "[-" "$closer" "]-" "\n" "]L?" "$opener" "[L?-" "\n" "[-?" "$@"
fi

stripcslashes() {
    REPLY="$1"
    REPLY=${REPLY//\\\\/$'\001'} # временный маркер
    REPLY=${REPLY//\\n/$'\n'}
    REPLY=${REPLY//\\r/$'\r'}
    REPLY=${REPLY//\\t/$'\t'}
    REPLY=${REPLY//$'\001'/\\}
}

# Загружаем данные
if [[ "$file" == "-" ]]; then
    data=$(cat)
else
    data=$(<"$file")
fi

# Сохраняем исходные аргументы скрипта
IN_ARGS=("$@")

startfrom=0
datalen=${#data}
left=0
right=$datalen

while (( $# )); do
    needle="$1"
    action="$2"
    shift 2

    # Обработка escape-последовательностей (отменяется флагом =)
    if [[ "$action" != *"="* ]]; then
        stripcslashes "$needle"
        needle="$REPLY"
    fi

    # echo "Searching for '$needle' with action '$action' in range [$left, $right]"

    part=${data:left:right-left}
    case "$part" in
    *"$needle"*)
        # если подстрока найдена
        if [[ "$action" == *"L"* ]]; then
            # последнее вхождение
            after=${part##*"$needle"}
            pos=$(( left + ${#part} - ${#after} - ${#needle} ))
        else
            # первое вхождение
            before=${part%%"$needle"*}
            pos=$(( left + ${#before} ))
        fi
        ;;
    *)
        # если подстрока не найдена
        # Флаг "@" запускает поиск сначала поставив указатель начала поиска после текущей секции
        if [[ "$action" == *"@"* ]]; then
            left=$right
            if (( startfrom >= left )); then
                exit 1
            fi

            if [[ "$action" == *"."* ]]; then
                # Отладочный механизм, дампит пропускаемую секцию
                printf '\nSkip [[\n%s\n]] skip\n' "${part}"
            fi

            startfrom=$left
            right=$datalen
            # восстанавливаем строку аргументов и начинаем всё с начала
            set -- "${IN_ARGS[@]}"
            continue
        fi

        # Флаг "?" делает поиск опциональным
        if [[ "$action" == *"?"* ]]; then
            continue
        else
            exit 1
        fi
        ;;
    esac

    # мы здесь только если подстрока найдена, и мы решаем что делать дальше.

    case "$action" in
    \[*)
        # Флаг "-" исключает подстроку из секции (сдвигает начало за подстроку)
        if [[ "$action" == *"-"* ]]; then
            left=$(( pos + ${#needle} ))
        else
            left=$pos
        fi
        ;;
    \]*)
        # Флаг "-" исключает подстроку из секции (сдвигает конец перед подстрокой)
        if [[ "$action" == *"-"* ]]; then
            right=$pos
        else
            right=$(( pos + ${#needle} ))
        fi
        ;;
    \@*)
        # Флаг "@" без "[" или "]" в случае успеха означает что мы просто идём дальше.
        if [[ "$action" == *"+"* ]]; then
            # расширяет правый указатель секции до конца данных
            right=$datalen
        fi

        if [[ "$action" == *"."* ]]; then
            # Отладочный механизм, дампит найденную секцию
            printf '\nFound [[\n%s\n]] found\n' "${part}"
        fi

        if [[ "$action" == *"-"* ]]; then
            # если хотим, чтобы дальнейшие действия выполнялись над данными найденной секции.
            # берем данные секции в качестве нового контента и ведём дальше действия над ним.
            data=${data:left:right-left}
            datalen=${#data}
            left=0
            right=$datalen
            startfrom=0
        fi

        # сохраняем оставшиеся аргументы скрипта (на случай повторного использования "@")
        IN_ARGS=("$@")
        ;;
    *)
        echo "Bad action: $action" >&2
        exit 2
        ;;
    esac
done

(( left <= right )) || exit 1

if (( pos_only )); then
    printf '%s %s\n' "$left" "$right"
    exit 0
fi

if (( before_only )); then
    right=$left
    left=0
fi

if (( after_only )); then
    left=$right
    right=$datalen
fi

if (( replace_only )); then
    # Заменяем найденную секцию на данные из replace_data
    new_data="${data:0:left}${replace_data}${data:right}"
    printf '%s' "$new_data"
    exit 0
fi

if (( strip_prefix )) || [[ -n "$add_prefix" ]]; then
    _output="${data:left:right-left}"
    while IFS= read -r line; do
        printf '%s\n' "$add_prefix${line:strip_prefix}"
    done <<< "$_output"
else
    printf '%s' "${data:left:right-left}"
fi
