Как извлечь строки в кавычках из переменной?

Вопрос или проблема

Я признаю, что здесь ранее задавались внешне похожие вопросы, но все из тех, что я видел, проще того, что я пытаюсь достичь. Предпочтительны решения, основанные только на Bash.

У меня есть переменная, содержащая строку, которая выглядит как некоторое сравнение, и я хотел бы разделить её на массив. Вот несколько примеров, включая то, как я хотел бы их разделить:

var="name="value""                # arr=([0]=name [1]='=' [2]=value)
var="name != '!value=""           # arr=([0]=name [1]="!=' [2]='!value=")
var=""na=me" = value'             # arr=([0]=na=me [1]='=' [2]=value)
var="name >= value"               # arr=([0]=name [1]='>=' [2]=value)
var="name"                        # arr=([0]=name)
var="name = "escaped \"quotes\""" # arr=([0]=name [1]='=' [2]=escaped\ \"quotes\")
var="name = \"nested 'quotes'\""  # arr=([0]=name [1]='=' [2]=nested\ \'quotes\')
var="name="nested \"quotes\"""  # arr=([0]=name [1]='=' [2]=nested\ \"quotes\")

Как вы видите. Любая сторона (или ни одна) может быть в кавычках, как в одиночных, так и в двойных. Возможно наличие экранированных или других вложенных кавычек. Оператор между ними может быть любым из набора, но они также могут быть включены в строки в кавычках. Возможно наличие или отсутствие пробелов. Может вообще не быть оператора.

Мне нужно разобрать много строк, и поэтому я предпочел бы не создавать новый процесс каждый раз, именно поэтому предпочтительны решения, основанные только на Bash. Это дополнение к существующему скрипту Bash, который не нуждается в переносе на другие оболочки, и он работает на Bash 5.2, так что у меня есть доступ к современным функциям Bash, которые могут быть полезны.

IFS=\" read -a arr <<<"$var" хорош тем, что он понимает, как обрабатывать экранированные кавычки, и если бы мне нужно было иметь дело только с одиночными или двойными кавычками и не с обоими, я мог бы сделать это. Как обстоит дело, я просто надеюсь, что мне не придется писать целый алгоритм токенизации в скрипте оболочки, и что есть какое-то сочетание функций, которое я не учел, и которое может читать эту задачу надежно.

Вам нужно написать парсер: считывать строку символ за символом, в зависимости от текущего символа, расширять текущее слово или начинать новое. Держите флаг, чтобы указать, что парсер находится внутри строки в кавычках.

Что-то подобное:

#!/bin/bash
set -eu

validate() {
    size=$1
    shift

    if ((size != $#)) ; then
        echo "Не ОК # Неправильный размер: $size $#"
        return
    fi

    ok=1
    for ((j=1; j <= size; ++j)) ; do
        [[ ${!j} = ${arr[j-1]} ]] || ok=0
    done
    if ((ok)) ; then
        echo $i ОК
    else
        echo $i Не ОК
    fi
}

i=0
for var in 'name="value"'                \
           "name != '!value=""           \
           ""na=me" = value'             \
           'name >= value'               \
           'name'                        \
           'name = "escaped \"quotes\""' \
           "name = \"nested 'quotes'\""  \
           "name="nested \"quotes\"""  \
; do
    arr=()
    left=""
    quoted=""
    while ! (( ${#arr[@]} )) && [[ $var ]] ; do
        char=${var:0:1}
        var=${var:1}
        if [[ $char = [\'\"] ]] ; then
            if [[ -z $left ]] ; then
                quoted=$char
            elif [[ $quoted = $char ]] ; then
                quoted=${quoted:0:-1}
                arr=("$left")
            else
                echo 'Неожиданная кавычка' >&2
                exit 1
            fi
        elif [[ $char = [\ =!\>] && -z $quoted ]] ; then
            arr=("$left")
            if [[ $char != ' ' ]] ; then
                var=$char$var
            fi
        else
            left+=$char
        fi
    done
    arr=("$left")

    op=""
    arr[1]=""
    while [[ $var && ! ${arr[1]} ]] ; do
        char=${var:0:1}
        var=${var:1}
        if [[ $char = [=\<\>\!] ]] ; then
            op+=$char
        elif [[ $char=" " ]] ; then
            if [[ $op ]] ; then
                arr[1]=$op
            else
                :
            fi
        else
            arr[1]=$op
            var=$char$var
        fi
    done
    [[ -z ${arr[1]} ]] && unset arr[1]

    if [[ $var ]] ; then
        quoted=""
        right=""
        while [[ $var ]] ; do
            char=${var:0:1}
            var=${var:1}
            if [[ $quoted ]] ; then
                if [[ $char = ${quoted: -1} ]] ; then
                    quoted=${quoted:0:-1}
                elif [[ $char = \\ ]] ; then
                    nextchar=${var:0:1}
                    if [[ $nextchar = ${quoted: -1} ]] ; then
                        right+=$nextchar
                        var=${var:1}
                    fi
                else
                    right+=$char
                fi
            elif [[ $char = [\"\'] ]] ; then
                quoted+=$char
            else
                right+=$char
            fi
        done
        arr+=("$right")
    fi

    case $i in
        (0) exp=(name = value) ;;
        (1) exp=(name '!=' '!value=") ;;
        (2) exp=(na=me = value) ;;
        (3) exp=(name ">=' value) ;;
        (4) exp=(name) ;;
        (5) exp=(name="escaped "quotes"") ;;
        (6) exp=(name = "nested 'quotes'") ;;
        (7) exp=(name="nested "quotes"") ;;
        (*) exit 1 ;;
    esac

    validate ${#arr[@]} "${exp[@]}"

    ((++i))
done

Он корректно разбирает все примеры, которые вы привели, но это далеко не завершено (он не проверяет незакрытые кавычки и т.д.)

Я не думаю, что вы можете обходиться без написания лексера для разбиения ваших строк. Лично я бы использовал perl, но если вы действительно предпочитаете bash, тогда вы можете использовать регулярное выражение для “сканирования” $var токен за токеном:

#!/bin/bash

for var in ...

arr=()
while [[ $var =~ ^[[:space:]]*([[:alnum:]_]+|\'[^\']*\'|\"(\\.|[^\"\\])*\"|[^[:space:][:alnum:]_\"\']+) ]]
do
    var=${var:${#BASH_REMATCH[0]}}
    tok=${BASH_REMATCH[1]}
    case ${tok:0:1} in
    ( \" ) tok=${tok//\\\"/\"} ;&
    ( \' ) tok=${tok:1:-1}     ;;
    esac
    arr+=("$tok")
done
declare -p arr

done

примечание: требуется bash 4.3+

вывод:

declare -a arr=([0]="name" [1]="=" [2]="value")
declare -a arr=([0]="name" [1]="!=" [2]="!value=")
declare -a arr=([0]="na=me" [1]="=" [2]="value")
declare -a arr=([0]="name" [1]=">=" [2]="value")
declare -a arr=([0]="name")
declare -a arr=([0]="name" [1]="=" [2]="escaped \"quotes\"")
declare -a arr=([0]="name" [1]="=" [2]="nested 'quotes'")
declare -a arr=([0]="name" [1]="=" [2]="nested \"quotes\"")

Некоторые недостатки, которые читателю следует исправить:

  • Я сделал некоторые предположения о том, что должно быть “словом” и “оператором”. В основном, “слово” состоит из буквенно-цифровых (плюс подчеркивание) символов; и “оператор” может быть чем угодно, что не содержит пробелов (исключая слово и строку в кавычках).

  • Отсутствует проверка ошибок разбора, но тестирование на пустоту $var в конце должно быть достаточным.

  • Только \" декодируется в строках в двойных кавычках; возможно, вам нужно поддерживать множество других последовательностей экранирования.

  • Я не разделил регулярное выражение, но для удобочитаемости вы должны объявить некоторые переменные; например, space="[[:space:]]+" word='[[:alnum:]_]+' ...; [[ $var =~ $space($word|...) ]]

Ответ или решение

Чтобы извлечь строковые выражения, заключенные в кавычки, из переменной в Bash, нужно, во-первых, понять структуру задачи. Вы хотите разбить строку, содержащую выражения с операторами и переменными, на массив, учитывая, что каждое из этих выражений может быть окружено кавычками (одинарными или двойными). Как вы верно заметили, стандартные методы обработки строк в Bash позволяют решить лишь часть подобных задач из-за необходимости учета как нескольких видов кавычек, так и сложных случаев с вложенными и экранированными кавычками.

Теория:

В Bash отсутствуют встроенные мощные средства обработки строк, как, например, в Python или Perl, но к вашим услугам богатый арсенал регулярок и аритметических выражений. Основа решения заключается в грамотном разборе строки на составляющие с использованием циклов, регулярных выражений и нужного учета символов в кавычках. Одной из главных сложностей является поддержка вложенных кавычек и экранированных символов, что добавляет значительные требования к надежности алгоритма.

Пример:

В приведенных примерах использования:

  • var="name="value"" должен разбиться на массив arr=([0]=name [1]='=' [2]=value).
  • var="name != '!value="" должен разбиться на массив arr=([0]=name [1]="!=' [2]='!value=").

Для начала можно воспользоваться уже предложенными подходами, которые заключаются в пошаговом разборе строки, используя регулярные выражения, а также условные конструкции для учета кавычек. Например, подход с использованием регулярного выражения, который извлекает токены (слова, операторы и строки в кавычках), выглядит следующим образом:

#!/bin/bash

for var in ...

arr=()
while [[ $var =~ ^[[:space:]]*([[:alnum:]_]+|\'[^\']*\'|\"(\\.|[^\"\\])*\"|[^[:space:][:alnum:]_\"\']+) ]]
do
    var=${var:${#BASH_REMATCH[0]}}
    tok=${BASH_REMATCH[1]}
    case ${tok:0:1} in
    ( \" ) tok=${tok//\\\"/\"} ;&
    ( \' ) tok=${tok:1:-1}     ;;
    esac
    arr+=("$tok")
done
declare -p arr

done

Применение:

Этот фрагмент скрипта подойдет, если необходимо систематически обрабатывать множество строк в Bash, разложенных по заданной структуре. Его основная задача — обработать каждый элемент строки, привести его к нужному формату и, сохраняя порядок, добавить в результирующий массив. Такой подход позволяет минимизировать количество запусков сторонних программ, что особенно важно при обработке больших объемов данных.

При использовании этого метода вам стоит подумать об учете всех возможных конфигураций ввода, например:

  • Поддержка различных операторов, которые могут встретиться в выражении.
  • Корректная обработка строк с вложенными или неправильно закрытыми кавычками.
  • Корректный учет символов, которые могут быть экранированы.

Также не забывайте протестировать данный подход на различных строках, чтобы выявить и устранить возможные ошибки, которые могут возникнуть при выполнении скрипта.

В заключение, важно отметить, что написание полностью функционального лексера на Bash требует внимательного подхода и скрупулезного тестирования, что обеспечит надежную и эффективную обработку сложных строковых выражений с различными вложенными структурами данных.

Оцените материал
Добавить комментарий

Капча загружается...