Вопрос или проблема
Я признаю, что здесь ранее задавались внешне похожие вопросы, но все из тех, что я видел, проще того, что я пытаюсь достичь. Предпочтительны решения, основанные только на 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 требует внимательного подхода и скрупулезного тестирования, что обеспечит надежную и эффективную обработку сложных строковых выражений с различными вложенными структурами данных.