Параметр bash для разделения слов по пробелам с учетом и сохранением кавычек

Учитывая параметр 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

Вы можете посмотреть опцию CSV в GNU awk, Ruby, Python, Perl или mlr.

dawg 15.07.2024 01:00

@dawg мои требования к ОС были расплывчатыми, но у меня нет универсально доступных GNU awk, Ruby или mlr (AIX). Python может быть проблематичным, поскольку в некоторых системах по умолчанию установлена ​​версия 3 (macOS), а в некоторых — версия 2, но это может работать. Хотя, возможно, это не самое простое решение.

Zorawar 15.07.2024 01:12

Странно, что вы хотите, чтобы двойные кавычки интерпретировались как метасимволы (влияющие на значимость последующих пробелов), но вы также хотите сохранить их как часть реальных данных. В противном случае вы могли бы просто построить и вычислить простое объявление массива, например. eval "declare -a foo_transformed=($foo)".

pmf 15.07.2024 01:20

@pmf Действительно. Но я имею дело с устаревшими системами, которые имели хакерские решения, и это, к сожалению, мои требования.

Zorawar 15.07.2024 01:29
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
4
155
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

Идеальный. Большое спасибо. На самом деле я полностью пренебрег =~, но, если у кого-то нет более простого решения, я думаю, что оно (или его варианты) подойдет. Спасибо.

Zorawar 15.07.2024 02:36

Отличное решение! Просто хочу отметить, что для (несколько патологического) ввода s='"a"b"c' это, похоже, застряло в цикле. Возможно, имело бы смысл выйти из цикла после ${#foo}-й итерации с неверным входным сообщением об ошибке.

user1934428 15.07.2024 08:41

Вы можете использовать замену строки в операторе 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 15.07.2024 13:27

@jhnc Ты абсолютно прав. Затем я добавил побег для всех этих расширений.

Léa Gris 15.07.2024 14:29

также обратные кавычки и, возможно, арифметические контексты

jhnc 15.07.2024 14:53

Добавлен escape для $ обратного кавычка и проверки безопасности всего, включая арифметическое расширение и его устаревший синтаксис $[1+1].

Léa Gris 15.07.2024 14:58

Это еще одна идея, которая мне не пришла в голову, спасибо! Но мне не хватает опции оболочки? Когда я пытаюсь выполнить вышеизложенное, первые escape-символы заменяются буквальным амперсандом.

Zorawar 15.07.2024 19:22

@Zorawar Возможно, используемая версия Bash слишком старая и не имеет функции захвата шаблонов &. Использование Bash 5.2.21 здесь

Léa Gris 15.07.2024 19:28

GNU bash, версия 5.0.17. Не могли бы вы рассказать мне результат shopt | grep '\<on\>' | awk '{ print $1 }', shopt -o | grep '\<on\>' | awk '{ print $1 }' и echo "$-" в вашей системе, пожалуйста.

Zorawar 15.07.2024 19:59

@Zorawar checkwinsize cmdhist complete_fullquote extquote force_fignore globasciiranges globskipdots hostcomplete interactive_comments patsub_replacement progcomp promptvars sourcepath и braceexpand hashall interactive-comments Вариант, который заставляет это работать: patsub_replacement

Léa Gris 15.07.2024 21:10

Ах, тогда это объясняет. Судя по всему, это было добавлено в версии 5.2 (см. также: unix.stackexchange.com/questions/733148/…). К сожалению, для меня это исключается, но в остальном это было очень чистое решение и без каких-либо явных циклов. В любом случае, спасибо.

Zorawar 15.07.2024 21:22
Ответ принят как подходящий

Это может быть то, что вы хотите:

$ 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:]"]*)+ — повторяющаяся строка подстрок в двойных кавычках, содержащая любые символы или не содержащая символов, при необходимости окруженная подстроками, не содержащими пробелов или двойных кавычек.

Думаю, для меня это полный ответ. Несовпадающая двойная кавычка будет интерпретироваться как образующая отдельное слово, но я рад, что это может привести к ошибке позже. Так что большое спасибо!

Zorawar 15.07.2024 20:03

Другие вопросы по теме