Разбить строку в массив в bash

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

строку:

Paris, France, Europe

Я хотел бы иметь их в массив, как это:

array[0] = Paris
array[1] = France
array[2] = Europe

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

15 ответов


IFS=', ' read -r -a array <<< "$string"

обратите внимание, что символы $IFS рассматриваются индивидуально как разделители, так что в этом случае поля могут быть разделены на или запятая или пробел, а не последовательность двух символов. Интересно, что пустые поля не создаются при появлении запятых во входных данных, потому что пространство обрабатывается специально.

для доступа к отдельному элементу:

echo "${array[0]}"

для перебора элементы:

for element in "${array[@]}"
do
    echo "$element"
done

чтобы получить индекс и значение:

for index in "${!array[@]}"
do
    echo "$index ${array[index]}"
done

последний пример полезен, потому что массивы Bash разрежены. Другими словами, вы можете удалить элемент или добавить элемент, а затем индексы не будут смежными.

unset "array[1]"
array[42]=Earth

получить количество элементов в массиве:

echo "${#array[@]}"

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

echo "${array[-1]}"

в любой версии Bash (откуда-то после 2.05 b):

echo "${array[@]: -1:1}"

большие отрицательные смещения выберите дальше от конца массива. Обратите внимание на пробел перед знаком минус в старой форме. Это необходимо.


вот способ без установки IFS:

string="1:2:3:4:5"
set -f                      # avoid globbing (expansion of *).
array=(${string//:/ })
for i in "${!array[@]}"
do
    echo "$i=>${array[i]}"
done

идея заключается в использовании замены строки:

${string//substring/replacement}

чтобы заменить все совпадения $substring пробелом, а затем использовать замещенную строку для инициализации массива:

(element1 element2 ... elementN)

Примечание: этот ответ использует split+glob оператор. Таким образом, чтобы предотвратить расширение некоторых символов (например,*) это хорошая идея, чтобы приостановить подстановка для этого сценария.


все ответы на этот вопрос являются неправильными в той или иной степени.


неверный ответ #1

IFS=', ' read -r -a array <<< "$string"

1: это злоупотребление $IFS. Значение $IFS переменная не взят одной переменной длиной разделитель строк, скорее он берется как set of один символов строковые сепараторы, где каждое поле что read разделение от входной линии может быть прекращено любой символ в наборе (запятая или space, в этом примере).

на самом деле, для настоящих приверженцев там, полное значение $IFS немного сложнее. От руководство bash:

оболочка обрабатывает каждый символ ИФС в качестве разделителя, и разбивает результаты из других расширений в слова, используя эти символы как терминаторы полей. Если ИФС не установлено, или его значение точно , по умолчанию, затем последовательности , и в начале и конце результатов предыдущих расширений игнорируются, и любая последовательность ИФС символы в начале или конце служит для разделения слов. Если ИФС имеет значение, отличное от значения по умолчанию, а затем последовательности пробелов, и игнорируются в начале и конце слова, пока символ пробела находится в значении ИФС (an ИФС символ пробела). Любой символ в ИФС это не ИФС пробел вместе с рядом ИФС пробелы, разделяющие поле. Последовательность ИФС пробелы также рассматриваются как разделитель. Если значение ИФС равно null, разделение слов не происходит.

в основном, для нестандартных ненулевых значений $IFS, поля могут быть разделены либо (1) последовательностью одного или нескольких символов, которые все из набора "IFS пробелов" (то есть, какой из , и ("строки", Что означает строки (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 переменная содержит любые LFs, затем read остановит обработку, как только он встретит первый LF. Этот read builtin обрабатывает только одну строку на вызов. Это верно, даже если вы трубопровод или перенаправление ввода только до read заявление, как мы делаем в этом примере с строка механизм, и таким образом гарантирован, что будет потерян необработанный входной сигнал. Код, который питает read builtin не имеет знаний о потоке данных в своей командной структуре.

вы можете возразить, что это вряд ли вызовет проблема, но все же, это тонкая опасность, которую следует избегать, если это возможно. Это вызвано тем, что read builtin фактически выполняет два уровня разделения ввода: сначала на строки, затем на поля. Поскольку OP хочет только один уровень разделения, это использование read builtin не подходит, и мы должны избегать этого.

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]="")

возможно, ОП не будет заботиться об этом, но это все еще ограничение, о котором стоит знать. Это снижает надежность и универсальность решения.

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

это решает проблему двух уровней разделения, совершенного read, так как расщепление слова само по себе составляет только один уровень расщепления. Но, как и раньше, проблема в том, что отдельные поля во входной строке уже могут содержать $IFS символы, и, таким образом, они будут неправильно разделены во время операции разделения слов. Это не относится ни к одной из выборочных входных строк, предоставляемых этими ответчиками (насколько это удобно...), но, конечно, это не меняет того факта, что любая кодовая база, которая использовала эту идиому, рисковала бы взорваться, если бы это предположение когда-либо было нарушено в какой-то момент вниз по линии. Еще раз рассмотреть мое контрпример 'Los Angeles, United States, North America' (или 'Los Angeles:United States:North America').

кроме того, разделение слов обычно сопровождается расширение имени файла (ака расширения путем ака globbing), который, если это будет сделано, потенциально испортит слова, содержащие символы *, ? или [ следовал по ] (и, если extglob установлен, в скобках фрагменты предшествуют ?, *, +, @ или !), сопоставляя их против объектов файловой системы и расширения слов ("globs") соответственно. Первый из этих трех ответчиков умело подрезал эту проблему, запустив set -f заранее, чтобы отключить globbing. Технически это работает (хотя вы, вероятно, должны добавить set +f потом включить подстановки для последующего кода, который может зависеть от этого), но это нежелательно связываться с глобальными настройками оболочки для того, чтобы взломать основной строку в массив при разборе операции в местной код.

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

Примечание: Если вы собираетесь использовать это решение, то лучше использовать ${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. Разница в том, что ответчик сделал предположение, что поля разделены двумя символами, один из которых представлен в default $IFS, а другой нет. Он решил этот довольно конкретный случай, удалив символ, не представленный IFS, используя расширение подстановки шаблона, а затем использование разбиения слов для разбиения полей на уцелевший символ разделителя, представленный IFS.

это не очень универсальное решение. Кроме того, можно утверждать, что запятая действительно является "первичным" символом разделителя здесь, и что ее удаление, а затем в зависимости от символа пространства для расщепления поля просто неправильно. Еще раз рассмотрим мой контрпример:--34-->.

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

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. Но когда ... --82--> запускает свой код, он запускает его в среде оболочки, как если бы это был обычный статический исходный код, и поэтому мы можем запустить $array назначение внутри eval аргумент, чтобы он вступил в силу в среде оболочки, в то время как $IFS назначение префикса, который начинается с eval команда не переживет , расширение параметров и переменных, команду, арифметические расширения, разбиение и расширения путем.

порядок расширений: расширение скобки; расширение Тильды, расширение параметров и переменных, арифметическое расширение и подстановка команд (выполняется слева направо); разделение слов; и расширение пути.

вы могли бы поспорить версия GNU руководства делает немного лучше, так как он выбирает слово "токены" вместо "слова" в первом предложении раздела расширения:

расширение выполняется в командной строке после ее разделения на токены.

важный момент,$IFS не изменяет способ анализа исходного кода bash. Разбор источника bash код на самом деле очень сложный процесс, который включает в себя распознавание различных элементов грамматики оболочки, таких как последовательности команд, списки команд, конвейеры, расширения параметров, арифметические замены и замены команд. По большей части процесс синтаксического анализа bash не может быть изменен действиями на уровне пользователя, такими как назначения переменных (на самом деле, есть некоторые незначительные исключения из этого правила; например, см. various compatxx настройки оболочки, который может изменить некоторые аспекты разбора поведения на лету). Восходящие "слова" / "токены", которые являются результатом этого сложного процесса синтаксического анализа, затем расширяются в соответствии с общим процессом" расширения", как разбито в приведенных выше выдержках документации, где разбиение расширенного (расширение?) текст в нижестоящие слова-это просто один шаг этого процесса. Разбиение слов касается только текста, который был выплюнут из предыдущего шага расширения; это не влияет на буквальный текст, который был проанализирован правильно отключен от источника bytestream.


неправильный ответ #7

string='first line
        second line
        third line'

while read -r line; do lines+=("$line"); done <<<"$string"

это одно из лучших решений. Обратите внимание, что мы вернулись к использованию read. Разве я не говорил раньше, что read неуместно, потому что он выполняет два уровня расщепления, когда нам нужен только один? Фокус здесь в том, что вы можете позвонить read таким образом, что он эффективно выполняет только один уровень расщепления, в частности разделение только одного поля на вызов, что требует затрат на повторный вызов в цикле. Это немного ловко, но работает.

но есть проблемы. Во-первых: когда вы предоставляете хотя бы один имя до read, он автоматически игнорирует ведущие и конечные пробелы в каждом поле, которое отделено от входной строки. Это происходит ли $IFS имеет значение по умолчанию или нет, как описано ранее в эта должность. Теперь OP может не заботиться об этом для своего конкретного случая использования, и на самом деле это может быть желательной особенностью поведения синтаксического анализа. Но не каждый, кто хочет разобрать строку на поля, захочет этого. Однако есть решение: несколько неочевидное использование read должен пройти ноль имя аргументов. В этом случае 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. Как и раньше, разделители multicharacter не поддерживаются, что является неудачным ограничение этого решения. Мы могли бы попытаться хотя бы разделить запятую, указав разделитель на -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, что является значением по умолчанию, когда вы не указываете и <<< ("here-string") механизм автоматически добавляет LF к строке непосредственно перед тем, как он подает ее в качестве ввода в команду. Следовательно, в этих случаях, мы вроде случайно решил проблему отброшенного конечного поля, невольно добавив дополнительный фиктивный Терминатор к входу. Назовем это решение решением "манекен-Терминатор". Мы можем применить решение dummy-terminator вручную для любого пользовательского разделителя, объединив его с входной строкой самостоятельно, когда создание экземпляра в here-string:

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 не удалось прочитать какие-либо символы до нажатия конца файла. Demo:

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, который автоматически добавляется к строке here <<< оператор перенаправления. Конечно, его можно было бы снять. отдельно через явную операцию обрезки, как описано минуту назад, но, очевидно, ручной подход манекена-Терминатора решает его напрямую, поэтому мы могли бы просто пойти с этим. Ручное решение манекена-Терминатора на самом деле довольно удобно в том, что оно решает обе эти две проблемы (проблему отброшенного конечного поля и добавленную проблему LF) за один раз.

Итак, в целом, это довольно мощное решение. Это единственная оставшаяся слабость-отсутствие поддержки многосимвольная разделители, которые я затрону позднее.


неправильный ответ #8

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

(это на самом деле с того же поста, что и #7; ответчик предоставил два решения в одном и том же сообщении.)

на readarray builtin, что является синонимом mapfile, это идеальный вариант. Это встроенная команда, которая анализирует bytestream в переменную массива за один снимок; нет возиться с петлями, условностями, заменами или чем-то еще. И он не тайно удаляет пробелы из входной строки. И (если -O не дается) он удобно очищает целевой массив перед назначением ему. Но это все еще не идеально, поэтому моя критика его как "неправильного ответа".

во-первых, просто чтобы убрать это с пути, обратите внимание, что, как и поведение read при выполнении анализа полей,readarray удаляет поле трейлинга, если оно пустое. Опять же, это, вероятно, не касается OP, но это может быть для некоторых случаев использования. Я вернусь к этому вопросу.

во-вторых, как и раньше, он не поддерживает разделители multicharacter. Я тоже сейчас все исправлю.

в-третьих, решение, как написано, не анализирует входную строку OP, и на самом деле его нельзя использовать как-есть для ее анализа. Я и об этом сейчас расскажу.

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


правильный ответ

вот наивная попытка сделать #8 работа, просто указав :

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?). Но если мы сможем взломать многохарактерный разделитель, то это решит обе проблемы за один выстрел.

к сожалению, нет прямые способ получить разделитель multicharacter для работы. Лучшее решение я придумал это для предварительной обработки входной строки, чтобы заменить многосимвольная разделитель один разделитель, который будет гарантировано не столкнуться с содержимым строке ввода. Единственным символом, который имеет эту гарантию, является нул байт. Это связано с тем, что в bash (хотя и не в zsh, кстати) переменные не могут содержать байт NUL. Этот шаг предварительной обработки можно выполнить в процесс замещения. Вот как это сделать с помощью на awk:

readarray -td '' a < <(awk '{ gsub(/, /,""); print; }' <<<"$string, "); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

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


решение для обрезки

наконец, я хотел продемонстрировать свое собственное довольно сложное решение для обрезки, используя неясное на readarray. К сожалению, у меня закончилось место против драконовского 30,000-символьного ограничения переполнения стека, поэтому я не смогу это объяснить. Я оставлю это упражнение для читателя.

function mfcb { local val=""; ""; eval "[]=$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")

t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

печатает три


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

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"

for line in "${lines[@]}"
    do
        echo "--> $line"
done

принятый ответ работает для значений в одну строку.
если переменная имеет несколько строк:

string='first line
        second line
        third line'

нам нужна совсем другая команда, чтобы получить все строки:

while read -r line; do lines+=("$line"); done <<<"$string"

или гораздо проще bash readarray:

readarray -t lines <<<"$string"

печать всех строк очень легко воспользоваться функцией printf:

printf ">[%s]\n" "${lines[@]}"

>[first line]
>[        second line]
>[        third line]

это похоже на подход Jmoney38, но с использованием sed:

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
echo ${array[0]}

печать 1


ключом к разделению строки на массив является мультисимвольный разделитель ", ". Любое решение с использованием IFS для разделителей с несколькими символами по своей сути неверно, поскольку IFS-это набор этих символов, а не строка.

при назначении IFS=", " тогда строка сломается на любом "," или " " или любая их комбинация, которая не является точным представлением двух символьных разделителей ", ".

можно использовать awk или sed чтобы разделить строку, с подстановкой процесса:

#!/bin/bash

str="Paris, France, Europe"
array=()
while read -r -d $'' each; do   # use a NUL terminated field separator 
    array+=("$each")
done < <(printf "%s" "$str" | awk '{ gsub(/,[ ]+|$/,""); print }')
declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output

более эффективно использовать регулярное выражение непосредственно в Bash:

#!/bin/bash

str="Paris, France, Europe"

array=()
while [[ $str =~ ([^,]+)(,[ ]+|$) ]]; do
    array+=("${BASH_REMATCH[1]}")   # capture the field
    i=${#BASH_REMATCH}              # length of field + delimiter
    str=${str:i}                    # advance the string by that length
done                                # the loop deletes $str, so make a copy if needed

declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output...

со второй формой нет суб-оболочки, и она будет по своей сути быстрее.


редактировать bgoldst: вот некоторые ориентиры, сравнивающие мои readarray решение для решения регулярных выражений dawg, и я также включил read решение для этого (примечание :Я немного изменил регулярное выражение решение для большей гармонии с моим решением) (Также см. мои комментарии ниже поста):

## competitors
function c_readarray { readarray -td '' a < <(awk '{ gsub(/, /,""); print; };' <<<", "); unset 'a[-1]'; };
function c_read { a=(); local REPLY=''; while read -r -d ''; do a+=("$REPLY"); done < <(awk '{ gsub(/, /,""); print; };' <<<", "); };
function c_regex { a=(); local s=", "; while [[ $s =~ ([^,]+),\  ]]; do a+=("${BASH_REMATCH[1]}"); s=${s:${#BASH_REMATCH}}; done; };

## helper functions
function rep {
    local -i i=-1;
    for ((i = 0; i<; ++i)); do
        printf %s "";
    done;
}; ## end rep()

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "" != ':' ]]; do
        func="";
        if [[ ! "$func" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then
            echo "bad function name: $func" >&2;
            return 2;
        fi;
        funcs+=("$func");
        shift;
    done;
    shift;
    args=("$@");
    for func in "${funcs[@]}"; do
        echo -n "$func ";
        { time $func "${args[@]}" >/dev/null 2>&1; } 2>&1| tr '\n' '/';
        rc=${PIPESTATUS[0]}; if [[ $rc -ne 0 ]]; then echo "[$rc]"; else echo; fi;
    done| column -ts/;
}; ## end testAll()

function makeStringToSplit {
    local -i n=; ## number of fields
    if [[ $n -lt 0 ]]; then echo "bad field count: $n" >&2; return 2; fi;
    if [[ $n -eq 0 ]]; then
        echo;
    elif [[ $n -eq 1 ]]; then
        echo 'first field';
    elif [[ "$n" -eq 2 ]]; then
        echo 'first field, last field';
    else
        echo "first field, $(rep $[-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

function testAll_splitIntoArray {
    local -i n=; ## number of fields in input string
    local s='';
    echo "===== $n field$(if [[ $n -ne 1 ]]; then echo 's'; fi;) =====";
    s="$(makeStringToSplit "$n")";
    testAll c_readarray c_read c_regex : "$s";
}; ## end testAll_splitIntoArray()

## results
testAll_splitIntoArray 1;
## ===== 1 field =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.000s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 10;
## ===== 10 fields =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.001s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 100;
## ===== 100 fields =====
## c_readarray   real  0m0.069s   user 0m0.000s   sys  0m0.062s
## c_read        real  0m0.065s   user 0m0.000s   sys  0m0.046s
## c_regex       real  0m0.005s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 1000;
## ===== 1000 fields =====
## c_readarray   real  0m0.084s   user 0m0.031s   sys  0m0.077s
## c_read        real  0m0.092s   user 0m0.031s   sys  0m0.046s
## c_regex       real  0m0.125s   user 0m0.125s   sys  0m0.000s
##
testAll_splitIntoArray 10000;
## ===== 10000 fields =====
## c_readarray   real  0m0.209s   user 0m0.093s   sys  0m0.108s
## c_read        real  0m0.333s   user 0m0.234s   sys  0m0.109s
## c_regex       real  0m9.095s   user 0m9.078s   sys  0m0.000s
##
testAll_splitIntoArray 100000;
## ===== 100000 fields =====
## c_readarray   real  0m1.460s   user 0m0.326s   sys  0m1.124s
## c_read        real  0m2.780s   user 0m1.686s   sys  0m1.092s
## c_regex       real  17m38.208s   user 15m16.359s   sys  2m19.375s
##

попробуй такое

IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done

Это просто. Если вы хотите, вы также можете добавить объявление (а также удалить запятые):

IFS=' ';declare -a array=(Paris France Europe)

IFS добавляется для отмены вышеизложенного, но он работает без него в новом экземпляре bash


используйте этот:

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe

вот мой Хак!

разделение строк по строкам-довольно скучная вещь, чтобы сделать с помощью bash. Что происходит, так это то, что у нас есть ограниченные подходы, которые работают только в нескольких случаях (разбитые на ";", "/", "."и так далее) или у нас есть различные побочные эффекты в выходах.

подход ниже потребовал ряда маневров, но я считаю, что он будет работать для большинства наших потребностей!

#!/bin/bash

# --------------------------------------
# SPLIT FUNCTION
# ----------------

F_SPLIT_R=()
f_split() {
    : 'It does a "split" into a given string and returns an array.

    Args:
        TARGET_P (str): Target string to "split".
        DELIMITER_P (Optional[str]): Delimiter used to "split". If not 
    informed the split will be done by spaces.

    Returns:
        F_SPLIT_R (array): Array with the provided string separated by the 
    informed delimiter.
    '

    F_SPLIT_R=()
    TARGET_P=
    DELIMITER_P=
    if [ -z "$DELIMITER_P" ] ; then
        DELIMITER_P=" "
    fi

    REMOVE_N=1
    if [ "$DELIMITER_P" == "\n" ] ; then
        REMOVE_N=0
    fi

    # NOTE: This was the only parameter that has been a problem so far! 
    # By Questor
    # [Ref.: https://unix.stackexchange.com/a/390732/61742]
    if [ "$DELIMITER_P" == "./" ] ; then
        DELIMITER_P="[.]/"
    fi

    if [ ${REMOVE_N} -eq 1 ] ; then

        # NOTE: Due to bash limitations we have some problems getting the 
        # output of a split by awk inside an array and so we need to use 
        # "line break" (\n) to succeed. Seen this, we remove the line breaks 
        # momentarily afterwards we reintegrate them. The problem is that if 
        # there is a line break in the "string" informed, this line break will 
        # be lost, that is, it is erroneously removed in the output! 
        # By Questor
        TARGET_P=$(awk 'BEGIN {RS="dn"} {gsub("\n", "3F2C417D448C46918289218B7337FCAF"); printf }' <<< "${TARGET_P}")

    fi

    # NOTE: The replace of "\n" by "3F2C417D448C46918289218B7337FCAF" results 
    # in more occurrences of "3F2C417D448C46918289218B7337FCAF" than the 
    # amount of "\n" that there was originally in the string (one more 
    # occurrence at the end of the string)! We can not explain the reason for 
    # this side effect. The line below corrects this problem! By Questor
    TARGET_P=${TARGET_P%????????????????????????????????}

    SPLIT_NOW=$(awk -F"$DELIMITER_P" '{for(i=1; i<=NF; i++){printf "%s\n", $i}}' <<< "${TARGET_P}")

    while IFS= read -r LINE_NOW ; do
        if [ ${REMOVE_N} -eq 1 ] ; then

            # NOTE: We use "'" to prevent blank lines with no other characters 
            # in the sequence being erroneously removed! We do not know the 
            # reason for this side effect! By Questor
            LN_NOW_WITH_N=$(awk 'BEGIN {RS="dn"} {gsub("3F2C417D448C46918289218B7337FCAF", "\n"); printf }' <<< "'${LINE_NOW}'")

            # NOTE: We use the commands below to revert the intervention made 
            # immediately above! By Questor
            LN_NOW_WITH_N=${LN_NOW_WITH_N%?}
            LN_NOW_WITH_N=${LN_NOW_WITH_N#?}

            F_SPLIT_R+=("$LN_NOW_WITH_N")
        else
            F_SPLIT_R+=("$LINE_NOW")
        fi
    done <<< "$SPLIT_NOW"
}

# --------------------------------------
# HOW TO USE
# ----------------

STRING_TO_SPLIT="
 * How do I list all databases and tables using psql?

\"
sudo -u postgres /usr/pgsql-9.4/bin/psql -c \"\l\"
sudo -u postgres /usr/pgsql-9.4/bin/psql <DB_NAME> -c \"\dt\"
\"

\"
\list or \l: list all databases
\dt: list all tables in the current database
\"

[Ref.: https://dba.stackexchange.com/questions/1285/how-do-i-list-all-databases-and-tables-using-psql]


"

f_split "$STRING_TO_SPLIT" "bin/psql -c"

# --------------------------------------
# OUTPUT AND TEST
# ----------------

ARR_LENGTH=${#F_SPLIT_R[*]}
for (( i=0; i<=$(( $ARR_LENGTH -1 )); i++ )) ; do
    echo " > -----------------------------------------"
    echo "${F_SPLIT_R[$i]}"
    echo " < -----------------------------------------"
done

if [ "$STRING_TO_SPLIT" == "${F_SPLIT_R[0]}bin/psql -c${F_SPLIT_R[1]}" ] ; then
    echo " > -----------------------------------------"
    echo "The strings are the same!"
    echo " < -----------------------------------------"
fi

другой способ сделать это без изменения IFS:

read -r -a myarray <<< "${string//, /$IFS}"

вместо изменения IFS в соответствии с нашим желаемым разделителем,мы можем заменить все вхождения нашего желаемого разделителя ", " с содержанием $IFS via "${string//, /$IFS}".

может быть, это будет медленно для очень больших строк?

это основано на ответе Денниса Уильямсона.


другой подход может быть:

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

после этого ' arr ' представляет собой массив с четырьмя строками. Это не требует обработки IFS или чтения или любого другого специального материала, следовательно, намного проще и прямо.


UPDATE: Не делайте этого из-за проблем с eval.

С чуть меньшей церемонией:

IFS=', ' eval 'array=($string)'

например

string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar

еще один способ:

string="Paris, France, Europe"
IFS=', ' arr=(${string})

теперь ваши элементы хранятся в массиве "arr". Для перебора элементов:

for i in ${arr[@]}; do echo $i; done