В скрипте Bash я хотел бы разбить строку на части и сохранить их в массиве.
Я хотел бы использовать простой код, скорость команды не имеет значения. Как мне это сделать?
Ответ 3
Все ответы на этот вопрос так или иначе ошибочны.
Неверный ответ # 1
IFS=', ' read -r -a array <<< "$string"
1: Это неправильное использование $IFS
. Значение переменной $IFS
не, взятое за один разделитель строк переменной длины, скорее, оно берется как набор односимвольных разделителей строк, где каждое поле, которое read
отделяется из строки ввода может быть завершен любым символом в наборе (запятая или пробел, в этом примере).
Собственно, для настоящих приверженцев там полный смысл $IFS
немного более востребован. Из руководства bash:
Оболочка рассматривает каждый символ IFS как разделитель и разбивает результаты других расширений на слова, используя эти символы в качестве терминаторов полей. Если IFS не задано, или его значение равно <space> <tab> <newline> , по умолчанию, затем последовательности <space> , <tab> и <newline> в начале и конце результатов предыдущих расширений игнорируются, и любая последовательность IFS не в начале или в конце служит для разграничения слов. Если IFS имеет значение, отличное от значения по умолчанию, то последовательности символов пробела <space> , <tab> и <newline> игнорируются в начале и в конце слова, если символ пробела находится в значении IFS (пробел IFS персонаж). Любой символ в IFS, который не является IFS пробелом, а также любыми смежными символами пробела IFS, ограничивает поле. Последовательность символов пробела IFS также рассматривается как разделитель. Если значение IFS равно null, словосочетание не происходит.
В принципе, для ненулевых значений $IFS
, отличных от значения по умолчанию, поля могут быть разделены либо (1) последовательностью одного или нескольких символов, которые являются всеми из набора "символов пробела IFS" (то есть, в зависимости от <space> , <tab> и <newline> ( "новая строка" означает line feed (LF)) присутствуют где-либо в $IFS
) или (2) любой несимвольный символ IFS, который присутствует в $IFS
, а также все, что угодно "IFS пробельные символы" окружают его в строке ввода.
Для OP возможно, что второй режим разделения, описанный в предыдущем абзаце, является именно тем, что он хочет для своей входной строки, но мы можем быть уверены, что первый режим разделения, который я описал, не совсем прав. Например, что, если его входная строка была 'Los Angeles, United States, North America'
?
IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -p a;
## declare -a a=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America")
2: Даже если вы должны использовать это решение с разделителем с одним символом (например, с запятой, то есть без следующего пробела или другого багажа), если значение переменная $string
содержит любые LF, тогда read
перестанет обрабатываться после того, как она встретит первый LF. Компонент read
обрабатывает только одну строку для каждого вызова. Это справедливо даже в том случае, если вы выполняете пересылку или перенаправление ввода только в оператор read
, как мы делаем в этом примере с here-string механизм, и, следовательно, необработанный вход гарантированно будет потерян. Код, который управляет встроенным read
, не знает о потоке данных в его содержащей структуре команд.
Вы можете утверждать, что это вряд ли вызовет проблему, но тем не менее это является едва заметной опасностью, которую следует избегать, если это возможно. Это вызвано тем, что встроенный read
фактически выполняет два уровня входного разделения: сначала в строки, а затем в поля. Поскольку OP только хочет один уровень расщепления, это использование встроенного read
не подходит, и мы должны его избегать.
3: Неочевидная потенциальная проблема с этим решением заключается в том, что read
всегда оставляет конечное поле, если оно пустое, хотя в противном случае оно сохраняет пустые поля. Вот демо:
string=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -p a;
## declare -a a=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="")
Может быть, OP не заботится об этом, но это все еще ограничение, о котором стоит знать. Это снижает надежность и общность решения.
Эту проблему можно решить, добавив фиктивный трейлинг-разделитель во входную строку непосредственно перед ее отправкой на read
, как я продемонстрирую позже.
Неверный ответ # 2
string="1:2:3:4:5"
set -f # avoid globbing (expansion of *).
array=(${string//:/ })
Аналогичная идея:
t="one,two,three"
a=($(echo $t | tr ',' "\n"))
(Примечание. Я добавил отсутствующие круглые скобки вокруг подстановки команд, которые, по-видимому, отсутствовал.)
Аналогичная идея:
string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
Эти решения используют разбиение слов в распределении массива для разделения строки на поля. Как ни странно, как и read
, разделение общего слова также использует специальную переменную $IFS
, хотя в этом случае подразумевается, что она установлена в значение по умолчанию <space> <tab> <newline> , и поэтому любая последовательность из одного или нескольких символов IFS (которые теперь являются пробельными символами) считаются разделителем полей.
Это решает проблему двух уровней расщепления, совершенных с помощью read
, так как разбиение слова само по себе представляет собой только один уровень расщепления. Но, как и прежде, проблема заключается в том, что отдельные поля во входной строке уже могут содержать символы $IFS
, и поэтому они будут неправильно разделены во время операции разделения слова. Это случается так, что это не так для любой из вводных строк примера, предоставляемых этими респондентами (насколько это удобно...), но, конечно, это не меняет того факта, что любая база кода, которая использовала эту идиому, затем подвергалась риску если это предположение когда-либо нарушалось в какой-то момент по линии. Еще раз рассмотрим мой контрпример от 'Los Angeles, United States, North America'
(или 'Los Angeles:United States:North America'
).
Кроме того, при расщеплении слов обычно следует расширение имени файла (так называемое расширение имени пути aka globbing), которое, если это было сделано, потенциально искажает слова, содержащие символы *
, ?
или [
, за которыми следует ]
(и, если extglob
установлено, в скобках помечены фрагменты, предшествующие ?
, *
, +
, @
, или !
), сопоставляя их с объектами файловой системы и соответственно расширяя слова ( "глобусы" ). Первый из этих трех ответчиков умело подорвал эту проблему, предварительно запустив set -f
, чтобы отключить подглаживание. Технически это работает (хотя вам, вероятно, следует добавить set +f
после этого, чтобы повторно использовать globbing для последующего кода, который может зависеть от него), но нежелательно связываться с глобальными настройками оболочки, чтобы взломать базовую операцию синтаксического анализа строки в массив в локальном коде.
Другая проблема с этим ответом заключается в том, что все пустые поля будут потеряны. Это может быть или не быть проблемой, в зависимости от приложения.
Примечание. Если вы собираетесь использовать это решение, лучше использовать форму ${string//:/ }
"подстановка шаблона" расширение параметра, вместо того, чтобы идти на вызов вызывать подстановку команд (которая расширяет оболочку), запускать конвейер и запускать внешний исполняемый файл (tr
или sed
), поскольку расширение параметра - это просто внутренняя операция оболочки. (Кроме того, для решений tr
и sed
входная переменная должна быть заключена в двойную кавычку внутри подстановки команды, иначе разделение слов вступит в силу в команде echo
и потенциально может испортиться с значениями поля. $(...)
форма подстановки команд предпочтительнее старой формы `...`
, поскольку она упрощает вложение подстановок команд и позволяет лучше выделять синтаксис текстовыми редакторами.)
Неверный ответ # 3
str="a, b, c, d" # assuming there is a space after ',' as in Q
arr=(${str//,/}) # delete all occurrences of ','
Этот ответ почти такой же, как # 2. Разница заключается в том, что ответчик сделал предположение, что поля разделены двумя символами, один из которых представлен в стандартном $IFS
, а другой нет. Он решил этот довольно конкретный случай, удалив символ, не являющийся IFS, используя расширение подстановки шаблона, а затем используя разбиение слов, чтобы разделить поля на оставшийся IFS-представленный символ-разделитель.
Это не очень общее решение. Более того, можно утверждать, что запятая на самом деле является "основным" символом-разделителем здесь, и что ее удаление, а затем в зависимости от пространственного символа для разделения поля просто неверно. Еще раз рассмотрим мой контрпример: 'Los Angeles, United States, North America'
.
Кроме того, расширение файла может испортить расширенные слова, но это можно предотвратить, временно отключив globbing для назначения с помощью set -f
, а затем set +f
.
Кроме того, все пустые поля будут потеряны, что может быть или не быть проблемой в зависимости от приложения.
Неверный ответ # 4
string='first line
second line
third line'
oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"
Это похоже на # 2 и # 3, поскольку он использует разбиение слов, чтобы выполнить задание, только теперь код явно устанавливает $IFS
, чтобы содержать только односимвольный разделитель полей, присутствующий во входной строке. Следует повторить, что это не может работать для многофакторных разделителей полей, таких как разделитель запятой OP. Но для односимвольного разделителя, такого как LF, используемого в этом примере, он фактически близок к совершенству. Поля не могут быть непреднамеренно разделены посередине, как мы видели с предыдущими неправильными ответами, и есть только один уровень расщепления, если требуется.
Одна из проблем заключается в том, что расширение имени файла приведет к повреждению затронутых слов, как описано ранее, хотя еще раз это можно решить, обернув критический оператор в set -f
и set +f
.
Другая потенциальная проблема заключается в том, что, поскольку LF квалифицируется как "символ пробела IFS", как было определено ранее, все пустые поля будут потеряны, как в # 2 и # 3. Разумеется, это не будет проблемой, если разделитель окажется несимвольным символом IFS, и в зависимости от приложения это может не иметь никакого значения, но это снижает общность решения.
Итак, подведем итог, предположив, что у вас есть односимвольный разделитель, и он либо является символом пробела IFS, либо вам не нужны пустые поля, и вы завершаете критический оператор в set -f
и set +f
, то это решение работает, но в противном случае нет.
(Кроме того, для информации, назначение LF переменной в bash может быть проще с помощью синтаксиса $'...'
, например IFS=$'\n';
.)
Неверный ответ # 5
countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"
Аналогичная идея:
IFS=', ' eval 'array=($string)'
Это решение фактически является перекрестком между # 1 (тем, что он устанавливает $IFS
в запятую) и # 2-4 (тем, что использует слово разделение на разбиение строки на поля). Из-за этого он страдает от большинства проблем, которые затрагивают все вышеупомянутые неправильные ответы, вроде как самый худший из всех миров.
Кроме того, что касается второго варианта, может показаться, что вызов eval
совершенно не нужен, поскольку его аргумент является строковым литералом с одной кавычкой и поэтому статически известен. Но на самом деле очень неочевидная выгода от использования eval
таким образом. Обычно, когда вы запускаете простую команду, состоящую только из присваивания переменной, то есть без фактического имени команды, следующего за ней, назначение вступает в силу в среде оболочки:
IFS=', '; ## changes $IFS in the shell environment
Это верно, даже если простая команда включает в себя несколько присваиваний переменных; снова, пока нет командного слова, все назначения переменных влияют на среду оболочки:
IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment
Но если присвоение переменной привязано к имени команды (мне нравится называть это "назначением префикса" ), то это не влияет на среду оболочки и вместо этого влияет только на среду выполняемой команды, независимо от того, является встроенным или внешним:
IFS=', ' :; ## : is a builtin command, the $IFS assignment does not outlive it
IFS=', ' env; ## env is an external command, the $IFS assignment does not outlive it
Соответствующая цитата из руководства bash:
Если имя команды не появляется, назначения переменных влияют на текущую среду оболочки. В противном случае переменные добавляются в среду исполняемой команды и не влияют на текущую среду оболочки.
Можно использовать эту функцию назначения переменных для временного изменения $IFS
, что позволяет нам избежать всего гаджета сохранения и восстановления, как это делается с переменной $OIFS
в первом варианте, Но задача, с которой мы сталкиваемся здесь, состоит в том, что команда, которую нам нужно запустить, сама по себе является простым присваиванием переменной, и поэтому она не будет включать командное слово, чтобы временное назначение $IFS
. Вы можете подумать о себе, ну почему бы просто не добавить командное слово no-op в оператор, например : builtin
, чтобы сделать $IFS
присвоение временно? Это не работает, потому что тогда временное назначение $array
было бы временным:
IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command
Итак, мы находимся в тупике, немного поймаем-22. Но когда eval
запускает свой код, он запускает его в среде оболочки, как если бы это был обычный статический исходный код, поэтому мы можем запустить назначение $array
внутри аргумента eval
, чтобы оно вступало в силу в среда оболочки, в то время как префикс $IFS
, префикс которого соответствует команде eval
, не оживит команду eval
. Это точно трюк, который используется во втором варианте этого решения:
IFS=', ' eval 'array=($string)'; ## $IFS does not outlive the eval command, but $array does
Итак, как вы можете видеть, это на самом деле довольно хитроумный трюк и точно выполняет то, что требуется (по крайней мере, в отношении выполнения назначения) довольно непрозрачным способом. Я вообще не против этого трюка вообще, несмотря на участие eval
; просто будьте осторожны, чтобы одинарная кавычка строки аргументов для защиты от угроз безопасности.
Но опять же, из-за "худшего из всех миров" агломерации проблем, это все еще неверный ответ на требование OP.
Неверный ответ # 6
IFS=', '; array=(Paris, France, Europe)
IFS=' ';declare -a array=(Paris France Europe)
Ум... что? OP имеет строковую переменную, которая должна анализироваться в массив. Этот "ответ" начинается с дословного содержимого входной строки, вставленной в литерал массива. Я думаю, это один из способов сделать это.
Похоже, что ответчик предположил, что переменная $IFS
влияет на все синтаксические разборы bash во всех контекстах, что неверно. Из руководства bash:
IFS Внутренний разделитель полей, который используется для разделения слов после расширения и разделения строк на слова с помощью команды read. Значение по умолчанию: <space> <tab> <newline> .
Таким образом, специальная переменная $IFS
фактически используется только в двух контекстах: (1) разбиение слова, которое выполняется после расширения (что означает не при анализе исходного кода bash), и (2) для разделения входных строк на слова посредством read
встроенный.
Позвольте мне попытаться сделать это более ясным. Я думаю, что было бы неплохо провести различие между синтаксическим разбором и исполнением. bash должен сначала проанализировать исходный код, который, очевидно, является синтаксическим событием, а затем он выполняет код, а именно, когда в изображение входит расширение. Расширение - это действительно событие исполнения. Кроме того, я рассматриваю описание переменной $IFS
, которую я только что цитировал выше; вместо того, чтобы говорить, что разбиение слов выполняется после расширения, я бы сказал, что разбиение слова выполняется во время расширения, или, возможно, даже более точно, разделение слов является частью процесса расширения. Фраза "расщепление слов" относится только к этапу расширения; он никогда не должен использоваться для ссылки на синтаксический анализ исходного кода bash, хотя, к сожалению, документы, похоже, много оборачивают слова "split" и "words". Вот соответствующий отрывок из версии linux.die.net руководства bash:
Расширение выполняется в командной строке после того, как оно было разделено на слова. Существует семь видов расширения: расширение скобки, расширение тильды, расширение параметра и переменной, подстановка команд, арифметическое расширение, разбиение слов и расширение пути.
Порядок разложений: расширение скобки; расширение тильды, расширение параметров и переменных, арифметическое расширение и подстановка команд (выполняется слева направо); расщепление слов; и расширение имени пути.
Вы можете утверждать, что версия GNU в руководстве немного улучшилась, так как она выбирает слово "токены" вместо "слов" в первое предложение раздела Expansion:
Расширение выполняется в командной строке после того, как оно было разделено на токены.
Важным моментом является то, что $IFS
не меняет способ bash анализирует исходный код. Анализ исходного кода bash на самом деле является очень сложным процессом, который включает в себя распознавание различных элементов грамматики оболочки, таких как последовательности команд, списки команд, конвейеры, расширения параметров, арифметические подстановки и подстановки команд. По большей части процесс синтаксического анализа bash не может быть изменен с помощью действий на уровне пользователя, таких как назначения переменных (на самом деле, есть некоторые незначительные исключения из этого правила, например, см. Различные compatxx
настройки оболочки, который может изменять некоторые аспекты синтаксического поведения на лету). Верхние "слова" / "токены", которые являются результатом этого сложного процесса синтаксического анализа, затем расширяются в соответствии с общим процессом "расширения", как описано в вышеприведенных выдержках документации, где разбиение слова расширенного (расширяющегося?) Текста на нисходящий поток слова - это всего лишь один шаг этого процесса. Разделение слов касается только текста, который выплевывался из предыдущего шага расширения; это не влияет на литеральный текст, который анализировался прямо из исходного потока.
Неверный ответ # 7
string='first line
second line
third line'
while read -r line; do lines+=("$line"); done <<<"$string"
Это одно из лучших решений. Обратите внимание, что мы вернулись к использованию read
. Разве я не сказал ранее, что read
не подходит, потому что он выполняет два уровня разделения, когда нам нужен только один? Трюк здесь заключается в том, что вы можете вызвать read
таким образом, чтобы он эффективно выполнял только один уровень разделения, в частности, разделяя только одно поле на вызов, что требует затрат на повторное вызов в цикле. Это немного ловкость руки, но она работает.
Но есть проблемы. Во-первых: когда вы предоставляете хотя бы один аргумент NAME для read
, он автоматически игнорирует начальное и конечное пробелы в каждом поле, которое отделяется от входной строки. Это происходит независимо от того, установлено ли значение $IFS
по умолчанию или нет, как описано выше в этом сообщении. Теперь OP может не заботиться об этом для своего конкретного случая использования, и на самом деле это может быть желательной особенностью поведения синтаксического анализа. Но не всем, кто хочет разбирать строку в полях, захочется этого. Однако есть решение: несколько неочевидное использование read
- это пройти нулевые аргументы NAME. В этом случае read
будет хранить всю входную строку, которую он получает из входного потока в переменной с именем $REPLY
, и, в качестве бонуса, она не лишает ведущее и конечное пустое значение от значения. Это очень надежное использование read
, которое я часто использовал в своей карьере программирования оболочки. Здесь демонстрируется разница в поведении:
string=$' a b \n c d \n e f '; ## input string
a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -p a;
## declare -a a=([0]="a b" [1]="c d" [2]="e f") ## read trimmed surrounding whitespace
a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]=" a b " [1]=" c d " [2]=" e f ") ## no trimming
Вторая проблема с этим решением заключается в том, что он фактически не затрагивает случай настраиваемого разделителя полей, например, запятую OP. Как и прежде, многоканальные разделители не поддерживаются, что является неудачным ограничением этого решения. Мы могли бы попытаться хотя бы разделить запятую, указав разделитель на параметр -d
, но посмотрите, что произойдет:
string='Paris, France, Europe';
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France")
Как и ожидалось, неучтенные окружающие пробелы были вытащены в значения поля, и, следовательно, это нужно было бы скорректировать впоследствии посредством операций обрезки (это также можно было бы сделать непосредственно в цикле while). Но есть еще одна очевидная ошибка: Европа отсутствует! Что с ним случилось? Ответ заключается в том, что read
возвращает код возврата с ошибкой, если он попадает в конец файла (в этом случае мы можем назвать его окончанием строки), не сталкиваясь с окончательным полевым терминатором в конечном поле. Это заставляет цикл while прерываться преждевременно, и мы теряем конечное поле.
Технически эта же ошибка затронула и предыдущие примеры; разница в том, что разделитель полей принимался за LF, который является значением по умолчанию, когда вы не указываете параметр -d
, а механизм <<<
( "здесь-строка" ) автоматически добавляет LF к строке перед тем, как он подаст его в качестве ввода команды. Следовательно, в этих случаях мы вроде бы случайно решили проблему отброшенного конечного поля, невольно добавляя дополнительный фиктивный терминатор к входу. Позвольте называть это решение "фиктивным терминатором". Мы можем применить решение фиктивного терминатора вручную для любого настраиваемого разделителя, объединив его со строкой ввода при создании экземпляра в этой строке:
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")
Там проблема решена. Другим решением является только разрыв цикла while, если оба (1) read
возвращаются с ошибкой, а (2) $REPLY
пуст, что означает, что read
не смог прочитать никаких символов перед ударом по концу файла. Демо-ролик:
a=(); while read -rd,|| [[ -n "$REPLY" ]]; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')
Этот подход также показывает скрытный LF, который автоматически добавляется к этой строке оператором перенаправления <<<
. Разумеется, его можно было бы разделить отдельно с помощью явной операции обрезки, как описано несколько минут назад, но, очевидно, подход ручного фиктивного терминатора решает его напрямую, поэтому мы могли бы просто пойти с этим. Решение ручного фиктивного терминатора на самом деле довольно удобно в том смысле, что оно решает обе эти проблемы (проблема с выпадающим полем и проблема с добавлением LF) за один раз.
Итак, в целом, это довольно мощное решение. Остается только слабость - это отсутствие поддержки многохарактерных разделителей, о которых я расскажу позже.
Неверный ответ # 8
string='first line
second line
third line'
readarray -t lines <<<"$string"
(Это на самом деле с того же поста, что и # 7; ответчик предоставил два решения в одном сообщении.)
Встроенный readarray
, который является синонимом mapfile
, идеален. Это встроенная команда, которая анализирует байтовый поток в переменной массива за один снимок; не возиться с циклами, условностями, заменами или чем-либо еще. И это не скрывает, что любые пробелы из входной строки. И (если -O
не задано), он удобно очищает целевой массив перед назначением ему. Но это все еще не идеально, поэтому моя критика в этом как "неправильный ответ".
Во-первых, просто чтобы это убрать, обратите внимание, что, как и поведение read
при выполнении синтаксического анализа полей, readarray
возвращает конечное поле, если оно пустое. Опять же, это, вероятно, не беспокоит ОП, но это может быть для некоторых случаев использования. Я вернусь к этому через мгновение.
Во-вторых, как и прежде, он не поддерживает многосимвольные разделители. Я также дам исправить это за мгновение.
В-третьих, решение, как написано, не анализирует входную строку OP, и на самом деле ее нельзя использовать как-это для ее анализа. Я также развожу это на этот раз.
По вышеуказанным причинам я все же считаю это "неправильным ответом" на вопрос ОП. Ниже я дам то, что считаю правильным ответом.
Правильный ответ
Здесь наивная попытка сделать # 8 работать, просто указав параметр -d
:
string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')
Мы видим, что результат идентичен результату, полученному в результате двойного условного подхода решения цикла read
, обсуждаемого в # 7. Мы можем почти решить это с помощью ручного фиктивного терминатора:
readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')
Проблема заключается в том, что readarray
сохранил конечное поле, поскольку оператор перенаправления <<<
приложил LF к входной строке, и поэтому конечное поле не было пустым (иначе оно было бы удалено). Мы можем позаботиться об этом, явно отключив окончательный элемент массива после факта:
readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")
Единственные две оставшиеся проблемы, которые на самом деле связаны между собой, - это (1) посторонние пробелы, которые необходимо обрезать, и (2) отсутствие поддержки многохарактерных разделителей.
Пробелы, конечно, можно было бы обрезать после (например, см. Как обрезать пробелы из переменной bash?). Но если мы можем взломать многозадачный разделитель, то это решит обе проблемы одним выстрелом.
К сожалению, нет прямого способа заставить многозадачный разделитель работать. Лучшее решение, о котором я подумал, состоит в том, чтобы предварительно обработать входную строку, чтобы заменить разделитель с несколькими символами на односимвольный разделитель, который гарантированно не будет сталкиваться с содержимым входной строки. Единственным символом, который имеет эту гарантию, является NUL byte. Это связано с тем, что в bash (хотя и не в zsh, кстати) переменные не могут содержать байт NUL. Этот шаг предварительной обработки можно сделать встроенным в замещение процесса. Здесь, как это сделать, используя awk:
readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; }' <<<"$string, "); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")
Там, наконец! Это решение не будет ошибочно разделять поля посередине, не будет вырезать преждевременно, не потеряет пустые поля, не испортит себя при расширении имени файла, не будет автоматически линять ведущие и конечные пробелы, не оставит нисходящий LF на конце, не требует циклов и не подходит для односимвольного разделителя.
Решение обрезки
Наконец, я хотел продемонстрировать свое собственное довольно сложное решение для обрезки, используя опцию -C callback
function mfcb { local val="$4"; "$1"; eval "$2[$3]=\$val;"; };
function val_ltrim { if [[ "$val" =~ ^[[:space:]]+ ]]; then val="${val:${#BASH_REMATCH[0]}}"; fi; };
function val_rtrim { if [[ "$val" =~ [[:space:]]+$ ]]; then val="${val:0:${#val}-${#BASH_REMATCH[0]}}"; fi; };
function val_trim { val_ltrim; val_rtrim; };
readarray -c1 -C 'mfcb val_trim a' -td, <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")