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

457

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

Линия:

Paris, France, Europe

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

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

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

  • 13
    Это # 1 Google хит , но есть противоречие в ответ , потому что вопрос , к сожалению , спрашивает о разграничении на , (запятая-пространстве) , а не один символ , такой как запятая. Если вас интересует только последнее, ответы здесь проще найти: stackoverflow.com/questions/918886/…
Теги:
arrays
split
string-split

17 ответов

872
Лучший ответ
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.05b):

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

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

  • 0
    Как я могу ссылаться на элементы?
  • 14
    Просто используйте IFS=', ' , тогда вам не нужно удалять пробелы отдельно. Тест: IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"
Показать ещё 34 комментария
210

Вот способ без установки 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. Таким образом, чтобы предотвратить расширение некоторых символов (например, *), рекомендуется приостановить глобусы для этого script.

  • 1
    Использовал этот подход ... пока я не наткнулся на длинную нить, чтобы разделить. 100% процессор больше минуты (потом я его убил). Жаль, потому что этот метод позволяет разбивать строку, а не какой-то символ в IFS.
  • 0
    100% процессорного времени на одну минуту мне кажется, что где-то должно быть что-то не так. Как долго была эта строка, размером в МБ или ГБ? Я думаю, обычно, если вам просто понадобится небольшой разделитель строк, вы хотите остаться в Bash, но если это огромный файл, я бы выполнил что-то вроде Perl, чтобы сделать это.
Показать ещё 13 комментариев
169

Все ответы на этот вопрос так или иначе ошибочны.


Неверный ответ # 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")
  • 3
    Также может быть полезно отметить (хотя по понятным причинам у вас не было места для этого), что опция -d для readarray впервые появляется в Bash 4.4.
  • 59
    Вы должны добавить «TL; DR: прокрутите 3 страницы, чтобы увидеть правильное решение в конце моего ответа»
Показать ещё 11 комментариев
58
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

Печать трех

  • 3
    Я на самом деле предпочитаю такой подход. Просто.
  • 4
    Я скопировал и вставил это, и оно не работало с echo, но работало, когда я использовал его в цикле for.
Показать ещё 6 комментариев
30

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

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
23

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

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]
  • 1
    Хотя не каждое решение подходит для любой ситуации, ваше упоминание о readarray ... заменило мои последние два часа на 5 минут ... вы получили мой голос
  • 0
    readarray - правильный ответ.
4

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

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

Печать 1

3

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

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

Вы можете использовать awk или sed для разделения строки с заменой процесса:

#!/bin/bash

str="Paris, France, Europe"
array=()
while read -r -d $'\0' each; do   # use a NUL terminated field separator 
    array+=("$each")
done < <(printf "%s" "$str" | awk '{ gsub(/,[ ]+|$/,"\0"); 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 regex, и я также включил в него решение read (примечание: я слегка изменил regex для большей гармонии с моим решением) (также см. мои комментарии ниже сообщения):

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

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

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "$1" != ':' ]]; do
        func="$1";
        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=$1; ## 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 $[$1-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

function testAll_splitIntoArray {
    local -i n=$1; ## 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
##
  • 0
    Очень классное решение! Я никогда не думал об использовании цикла в совпадении с регулярным выражением, изящном использовании $BASH_REMATCH . Это работает, и действительно избегает порождения подоболочек. +1 от меня. Однако, в порядке критики, само регулярное выражение немного неидеально, так как кажется, что вы были вынуждены дублировать часть токена-разделителя (в частности, запятую), чтобы обойти отсутствие поддержки не жадных множителей (также внешний вид) в ERE («расширенный» вкус регулярных выражений, встроенный в bash). Это делает его немного менее универсальным и надежным.
  • 0
    Во-вторых, я провел несколько сравнительных тестов, и, хотя производительность лучше, чем у других решений для небольших строк, она экспоненциально ухудшается из-за повторного восстановления строк, становясь катастрофической для очень больших строк. Смотрите мое редактирование вашего ответа.
Показать ещё 1 комментарий
1

Решение Pure Bash для многосимвольных разделителей.

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

Поскольку Google имеет тенденцию оценивать этот ответ в верхней части результатов поиска или рядом с ней, я хотел дать читателям четкий ответ на вопрос о разделителях из нескольких символов, поскольку он также упоминается по крайней мере в одном ответе.

Если вы ищете решение проблемы многосимвольного разделителя, я предлагаю ознакомиться с постом Mallikarjun M, в частности с ответом gniourf_gniourf, который предоставляет это элегантное чисто BASH-решение с использованием расширения параметров:

#!/bin/bash
str="LearnABCtoABCSplitABCaABCString"
delimiter=ABC
s=$str$delimiter
array=();
while [[ $s ]]; do
    array+=( "${s%%"$delimiter"*}" );
    s=${s#*"$delimiter"};
done;
declare -p array

Ссылка на цитируемый комментарий/ссылочную запись

Ссылка на процитированный вопрос: Как разбить строку на многосимвольном разделителе в bash?

1

Попробуйте это

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

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

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

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

0

Я наткнулся на этот пост, когда хотел разобрать входные данные, такие как: word1, word2,...

ничто из перечисленного не помогло мне. решил это с помощью awk. Если это кому-то поможет:

STRING="value1,value2,value3"
array='echo $STRING | awk -F ',' '{ s = $1; for (i = 2; i <= NF; i++) s = s "\n"$i; print s; }''
for word in ${array}
do
        echo "This is the word $word"
done
0

Еще один способ сделать это без изменения IFS:

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

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

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

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

0

Вот мой хак!

Разделение строк по строкам - довольно скучная вещь при использовании 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=$1
    DELIMITER_P=$2
    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 $0}' <<< "${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 $0}' <<< "'${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
0

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

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

#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe
  • 3
    Плохо: возможно разделение слов и расширение пути. Пожалуйста, не возвращайте старые вопросы с хорошими ответами, чтобы дать плохие ответы.
  • 2
    Это может быть плохой ответ, но это все еще правильный ответ. Флаггеры / рецензенты: Для неправильных ответов, таких как этот, downvote, не удаляйте!
Показать ещё 2 комментария
0

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

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

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

  • 0
    Тот же (к сожалению, распространенный) антипаттерн, как и другие ответы: разделение слов и расширение имени файла.
0

ОБНОВЛЕНИЕ: не делайте этого из-за проблем с eval.

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

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

например.

string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar
  • 4
    Eval это зло! не делай этого
  • 1
    Пфф. Нет. Если вы пишете сценарии, достаточно большие, чтобы это имело значение, вы делаете это неправильно. В коде приложения eval - это зло. В сценариях оболочки это распространено, необходимо и несущественно.
Показать ещё 4 комментария
-2

Другой способ:

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

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

for i in ${arr[@]}; do echo $i; done
  • 1
    Я освещаю эту идею в своем ответе ; см. неправильный ответ № 5 (вам может быть особенно интересно мое обсуждение трюка eval ). Ваше решение оставляет $IFS установленным после запятой в качестве значения запятой.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню