Учитывая параметр bash
foo='ab "cd" "e f" x = "1 2" '
Я хочу создать массив, эквивалентный
foo_transformed=( ab '"cd"' '"e f"' 'x = "1 2"' )
переносимым способом, что означает использование по умолчанию встроенных функций bash (v3+) или программ, доступных для большинства операционных систем (Linux, Unix, Cygwin). Простота и безопасность при (почти) произвольной входной строке желательны.
Вы можете предположить, что ввод не содержит одинарных кавычек '
или обратной косой черты \
, но может содержать произвольное количество пробельных символов, как там, где я хочу, чтобы они ограничивали строку, так и там, где они этого не делают (когда внутри двойных кавычек).
Если мы попробуем:
foo_transformed=( $foo )
тогда внутренние кавычки foo
не учитываются (for k in "${foo_transformed[@]}"; do echo "- $k"; done
):
- ab
- "cd"
- "e
- f"
- x = "1
- 2"
Если мы попробуем:
eval foo_transformed=( $foo )
тогда кавычки теряются:
- ab
- cd
- e f
- x=1 2
@dawg мои требования к ОС были расплывчатыми, но у меня нет универсально доступных GNU awk, Ruby или mlr (AIX). Python может быть проблематичным, поскольку в некоторых системах по умолчанию установлена версия 3 (macOS), а в некоторых — версия 2, но это может работать. Хотя, возможно, это не самое простое решение.
Странно, что вы хотите, чтобы двойные кавычки интерпретировались как метасимволы (влияющие на значимость последующих пробелов), но вы также хотите сохранить их как часть реальных данных. В противном случае вы могли бы просто построить и вычислить простое объявление массива, например. eval "declare -a foo_transformed=($foo)"
.
@pmf Действительно. Но я имею дело с устаревшими системами, которые имели хакерские решения, и это, к сожалению, мои требования.
re=$'[ \t]*(([^ \t"]+|"[^"]*")+)'
s=$foo
foo_transformed=()
while [[ $s =~ $re ]]; do
foo_transformed+=("${BASH_REMATCH[1]}")
s=${s#"${BASH_REMATCH[0]}"}
done
Регулярное выражение позволяет foo
содержать встроенные символы новой строки в двойных кавычках. Чтобы правильно обрабатывать другие символы новой строки, замените оба экземпляра \t
на \t\n
или, возможно, [:space:]
.
(Регулярное выражение Bash не принимает \t
, поэтому я использую синтаксис $'...\t...'
для преобразования в буквальные символы табуляции/новой строки.)
Как отмечено в комментариях, это будет зависать из-за неправильного ввода.
Чтобы принять неверный ввод, можно использовать это модифицированное регулярное выражение:
re=$'[ \t]*(([^ \t"]+|"[^"]*("|$))+)'
Альтернативно, предварительную проверку можно выполнить:
malformed='^([^"]+|"[^"]*")*"[^"]*$'
if [[ $foo =~ $malformed ]]; then
: ...
fi
Идеальный. Большое спасибо. На самом деле я полностью пренебрег =~
, но, если у кого-то нет более простого решения, я думаю, что оно (или его варианты) подойдет. Спасибо.
Отличное решение! Просто хочу отметить, что для (несколько патологического) ввода s='"a"b"c'
это, похоже, застряло в цикле. Возможно, имело бы смысл выйти из цикла после ${#foo}
-й итерации с неверным входным сообщением об ошибке.
Вы можете использовать замену строки в операторе declare
, которая за один раз сделает именно то, что вы хотите:
#!/usr/bin/env bash
# Init
foo='ab "cd" "e f" x = "1 2" * $(date) `date` $[1+1] $((1+1)) ((1+1)) foo=''"''$HOME''" bar = "a '\'' b"'
# If the patsub_replacement shell option is enabled using shopt,
# any unquoted instances of & in string are replaced with the
# matching portion of pattern.
shopt -s patsub_replacement
# Escape arithmetic variables, commands and glob expansions
foo_escaped=${foo//[()\*\?\\\$\`\']/\\&}
# Escape and preserve double-quotes as string delimiters
foo_escaped=${foo_escaped//\"/\"\\\"}
# Transform
declare -a foo_transformed = "($foo_escaped)"
# Debug declaration
declare -p foo_transformed
# Debug print
printf %s\\n "${foo_transformed[@]}"
Детальное объяснение:
${foo//\"/\"\\\"}
заменяет каждую двойную кавычку "
в foo
на "\"
, поэтому она остается в кавычках, но содержит саму кавычку, которую нужно сохранить.
declare -a foo_transformed = "(${foo//\"/\"\\\"})"
, использует преобразованную строку в объявлении массива declare -a variable = "(string)"
.
РЕДАКТИРОВАТЬ
Добавлен выход из всех расширений.
следите за glob и другими расширениями: foo='* $(date)'
@jhnc Ты абсолютно прав. Затем я добавил побег для всех этих расширений.
также обратные кавычки и, возможно, арифметические контексты
Добавлен escape для $ обратного кавычка и проверки безопасности всего, включая арифметическое расширение и его устаревший синтаксис $[1+1]
.
Это еще одна идея, которая мне не пришла в голову, спасибо! Но мне не хватает опции оболочки? Когда я пытаюсь выполнить вышеизложенное, первые escape-символы заменяются буквальным амперсандом.
@Zorawar Возможно, используемая версия Bash слишком старая и не имеет функции захвата шаблонов &
. Использование Bash 5.2.21 здесь
GNU bash, версия 5.0.17. Не могли бы вы рассказать мне результат shopt | grep '\<on\>' | awk '{ print $1 }'
, shopt -o | grep '\<on\>' | awk '{ print $1 }'
и echo "$-"
в вашей системе, пожалуйста.
@Zorawar checkwinsize cmdhist complete_fullquote extquote force_fignore globasciiranges globskipdots hostcomplete interactive_comments patsub_replacement progcomp promptvars sourcepath
и braceexpand hashall interactive-comments
Вариант, который заставляет это работать: patsub_replacement
Ах, тогда это объясняет. Судя по всему, это было добавлено в версии 5.2 (см. также: unix.stackexchange.com/questions/733148/…). К сожалению, для меня это исключается, но в остальном это было очень чистое решение и без каких-либо явных циклов. В любом случае, спасибо.
Это может быть то, что вы хотите:
$ cat tst.sh
#!/usr/bin/env bash
foo=' ab "cd"
"e f" '\'' x = "1 2"
z = "a b"7"c
d"8 $HOME
`date` * $(date) '
fpat='(^[[:space:]]*)([^[:space:]]+|([^[:space:]"]*"([^"]|"")*"[^[:space:]"]*)+)'
foo_transformed=()
while [[ "$foo" =~ $fpat ]]; do
foo_transformed+=( "${BASH_REMATCH[2]}" )
foo = "${foo:${#BASH_REMATCH[0]}}"
done
declare -p foo_transformed
$ ./tst.sh
declare -a foo_transformed=([0] = "ab" [1] = "\"cd\"" [2] = "\"e f\"" [3] = "'" [4] = "x=\"1 2\"" [5]=$'z = "a b"7"c\n d"8' [6] = "\$HOME" [7] = "\`date\`" [8] = "*" [9] = "\$(date)")
Я дал foo
некоторые дополнительные значения, включая подстановочные символы, одинарную кавычку, потенциальные ссылки на переменные, вставки команд в старом и новом стиле, переводы строк внутри и снаружи строк в кавычках, а также строки, содержащие несколько подстрок в кавычках, чтобы можно было более полно протестировать скрипт.
Использование приведенного выше регулярного выражения fpat
вдохновлено FPAT
GNU awk, которое используется для идентификации полей во входных данных. См., например, как оно используется в CSV, в Каков наиболее надежный способ эффективного анализа CSV с помощью awk?. Это соответствует:
(^[[:space:]]*)
— необязательная начальная последовательность пробелов (которые отбрасываются), за которой следует (...)
, соответствующий строке, которую мы действительно хотим захватить:[^[:space:]]+
— ряд непробелов|
- или([^[:space:]"]*"([^"]|"")*"[^[:space:]"]*)+
— повторяющаяся строка подстрок в двойных кавычках, содержащая любые символы или не содержащая символов, при необходимости окруженная подстроками, не содержащими пробелов или двойных кавычек.Думаю, для меня это полный ответ. Несовпадающая двойная кавычка будет интерпретироваться как образующая отдельное слово, но я рад, что это может привести к ошибке позже. Так что большое спасибо!
Вы можете посмотреть опцию CSV в GNU awk, Ruby, Python, Perl или mlr.