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

641

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

Линия:

Paris, France, Europe

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

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

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

ЛГН
источник
22
Это хит Google № 1, но в ответе есть противоречие, потому что, к сожалению, вопрос заключается в том, чтобы разделить , (запятую), а не один символ, такой как запятая. Если вас интересует только последнее, ответы здесь проще найти: stackoverflow.com/questions/918886/…
antak
Если вы хотите разобрать строку и не заботиться о том, чтобы она была в виде массива, cutполезно иметь в виду команду bash. Разделитель можно задать en.wikibooks.org/wiki/Cut Вы также можете извлечь данные из структуры записи фиксированной ширины. en.wikipedia.org/wiki/Cut_(Unix) computerhope.com/unix/ucut.htm
JGFMK,

Ответы:

1092
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}"

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

Приостановлено до дальнейшего уведомления.
источник
15
Просто используйте IFS=', ', тогда вам не нужно удалять пробелы отдельно. Тест:IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"
l0b0
4
@ l0b0: Спасибо. Я не знаю, о чем я думал. declare -p arrayКстати, мне нравится использовать для тестового вывода.
Приостановлено до дальнейшего уведомления.
1
Это не похоже на уважение кавычек. Например, France, Europe, "Congo, The Democratic Republic of the"это будет разделяться после Конго.
Исраэль Дов
2
@YisraelDov: Bash не может самостоятельно справиться с CSV. Это не может сказать разницу между запятыми внутри кавычек и теми, что вне их. Вам нужно будет использовать инструмент, который понимает CSV, такой как lib на языке более высокого уровня, например модуль csv в Python.
Приостановлено до дальнейшего уведомления.
5
str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str"будет разделен на array=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")записку. Так что это работает только с полями без пробелов, поскольку IFS=', 'это набор отдельных символов, а не разделитель строк.
Dawg
334

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


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

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

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

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

Оболочка обрабатывает каждый символ IFS как разделитель и разбивает результаты других расширений на слова, используя эти символы в качестве разделителей полей. Если IFS не установлен или его значение в точности равно <пробел> <tab> <новая строка> , значение по умолчанию, тогда последовательности <пробел> , <tab> и <newline> в начале и конце результатов предыдущих расширений игнорируются, и любая последовательность символов IFS не в начале или в конце служит для разделения слов. Если IFS имеет значение, отличное от значения по умолчанию, то последовательности символов пробела <space> , <tab> и <игнорируются в начале и конце слова, если символ пробела находится в значении IFS ( символ пробела IFS ). Любой символ в IFS, который не является пробелом IFS , вместе с любыми соседними символами пробела IFS разделяет поле. Последовательность пробельных символов IFS также рассматривается как разделитель. Если значение IFS равно нулю, разделение слов не происходит.

По сути, для ненулевых значений по умолчанию, отличных от NULL $IFS, поля могут быть отделены либо (1) последовательностью из одного или нескольких символов, которые все находятся в наборе «пробельных символов IFS» (то есть, в зависимости от <space> , <tab> и <newline> («новая строка », означающая перевод строки (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оператор, как мы делаем в этом примере с механизмом здесь-строки , и, следовательно, необработанный ввод гарантированно будет потерян. Код, обеспечивающий работу 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]="")

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

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

Кроме того, за разделением слов обычно следует расширение имени файла ( иначе имен файлов ака подстановки), который, если сделана, потенциально коррумпированные слова , содержащие символы *, ?или [следует ](и, если extglobустановлен, Скобки фрагменты предшествуют ?, *, +, @, или !) сопоставляя их с объектами файловой системы и расширяя слова ("globs") соответственно. Первый из этих трех ответчиков ловко подправил эту проблему, запустивset -f заранее, чтобы отключить сглаживание. Технически это работает (хотя вы, вероятно, должны добавить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 . Разница в том, что ответчик сделал предположение, что поля разделены двумя символами, один из которых представлен по умолчанию $IFS, а другой нет. Он решил этот довольно специфический случай, удалив не-IFS-представленный символ, используя расширение подстановки шаблонов, а затем используя разделение слов, чтобы разделить поля на оставшемся IFS-представленном символе-разделителе.

Это не очень общее решение. Кроме того, можно утверждать, что запятая на самом деле является «основным» символом разделителя, и что ее удаление и последующее использование символа пробела для разделения поля просто неверно. Еще раз рассмотрим мои контрпример: 'Los Angeles, United States, North America'.

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

Но опять же, из-за наихудшей агломерации проблем, это все еще неправильный ответ на требование ФП.


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

IFS=', '; array=(Paris, France, Europe)

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

Гм ... что? У OP есть строковая переменная, которую нужно проанализировать в массив. Этот «ответ» начинается с дословного содержимого входной строки, вставленной в литерал массива. Я думаю, это один из способов сделать это.

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

IFS     Внутренний разделитель полей, который используется для разделения слов после раскрытия и разделения строк на слова с помощью встроенной команды read . Значением по умолчанию является <пробел> <вкладка> <новая строка> .

Таким образом, $IFSспециальная переменная фактически используется только в двух контекстах: (1) разбиение слов, которое выполняется после раскрытия (то есть не при разборе исходного кода bash) и (2) для разбиения входных строк на слова readвстроенным.

Позвольте мне попытаться прояснить это. Я думаю, что было бы хорошо провести различие между разбором и выполнением . Bash должен сначала проанализировать исходный код, который, очевидно, является событием синтаксического анализа , а затем позже он выполняет код, когда происходит расширение. Расширение действительно является событием исполнения . Кроме того, я не согласен с описанием $IFSпеременной, которую я только что цитировал; Вместо того, чтобы говорить, что разделение слов выполняется после раскрытия , я бы сказал, что разделение слов выполняется во время раскрытия, или, возможно, даже более точно, разделение слов частьюпроцесс расширения. Фраза «расщепление слов» относится только к этому этапу расширения; его никогда не следует использовать для ссылки на синтаксический анализ исходного кода bash, хотя, к сожалению, документы, похоже, содержат много слов «split» и «words». Вот соответствующая выдержка из linux.die.net версии руководства по bash:

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

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

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

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

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

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

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

В общем, это довольно мощное решение. Единственный недостаток - отсутствие поддержки разделителей из нескольких символов, о которых я расскажу позже.


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

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

(Это на самом деле из того же поста, что и №7 ; ответчик предоставил два решения в одном и том же посте.)

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

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

Во-вторых, как и прежде, он не поддерживает разделители с несколькими символами. Я исправлю это через мгновение.

В-третьих, написанное решение не анализирует входную строку 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 . Это связано с тем, что в 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опцию readarray. К сожалению, мне не хватает места для драконовского предела в 30 000 символов в Stack Overflow, поэтому я не смогу это объяснить. Я оставлю это как упражнение для читателя.

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")
bgoldst
источник
8
Также может быть полезно отметить (хотя по понятным причинам у вас не было места для этого), что -dопция readarrayсначала появляется в Bash 4.4.
fbicknel
2
Отличный ответ (+1). Если вы измените свой awk на awk '{ gsub(/,[ ]+|$/,"\0"); print }'и удалите это объединение финала, ", " то вам не нужно проходить гимнастику для устранения финальной записи. Итак: readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")на Bash это поддерживает readarray. Обратите внимание , что ваш метод является Bash 4.4 и выше я думаю , что из-за -dвreadarray
Dawg
3
@datUser Это неудачно. Ваша версия bash должна быть слишком старой readarray. В этом случае вы можете использовать второе лучшее решение на основе read. Я имею в виду это: a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,";awkзаменой, если вам нужна поддержка разделителя нескольких символов). Дайте мне знать, если у вас возникнут какие-либо проблемы; Я уверен, что это решение должно работать на довольно старых версиях bash, начиная с версии 2, выпущенной два десятилетия назад.
bgoldst
1
Вау, какой блестящий ответ! Хи-хи, мой ответ: угробил скрипт bash и запустил python!
artfulrobot
1
@datUser Bash на OSX все еще застрял на 3.2 (выпущен около 2007 года); Я использовал bash, найденный в Homebrew, чтобы получить версии 4.X bash для OS X
JDS
222

Вот способ без настройки 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
Использовал этот подход ... пока я не наткнулся на длинную нить, чтобы разделить. 100% CPU больше минуты (потом я его убил). Жаль, потому что этот метод позволяет разбивать строку, а не какой-то символ в IFS.
Вернер Леманн
100% процессорного времени на одну минуту мне кажется, что где-то что-то не так. Как долго была эта строка, размером в МБ или ГБ? Я думаю, что обычно, если вам просто нужно небольшое разбиение строки, вы хотите остаться в Bash, но если это огромный файл, я бы выполнил что-то вроде Perl, чтобы сделать это.
12
ВНИМАНИЕ: Просто столкнулся с проблемой с этим подходом. Если у вас есть элемент с именем *, вы также получите все элементы вашего cwd. таким образом string = "1: 2: 3: 4: *" даст некоторые неожиданные и, возможно, опасные результаты в зависимости от вашей реализации. Не удалось получить ту же ошибку с (IFS = ',' read -a array <<< "$ string"), и эта ошибка кажется безопасной для использования.
Дитер Грибниц
4
цитирование ${string//:/ }предотвращает расширение оболочки
Эндрю Уайт
1
Мне пришлось использовать следующее на OSX: array=(${string//:/ })
Марк Томсон
96
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

Печатает три

Jmoney38
источник
8
Я на самом деле предпочитаю такой подход. Просто.
Креветка
4
Я скопировал и вставил это, и это не сработало с echo, но сработало, когда я использовал его в цикле for.
Бен
2
Это не работает, как указано. @ Jmoney38 или shrimpwagon, если вы можете вставить это в терминал и получить желаемый результат, пожалуйста, вставьте результат здесь.
abalter
2
@abalter работает для меня с a=($(echo $t | tr ',' "\n")). Тот же результат с a=($(echo $t | tr ',' ' ')).
лист
@procrastinator Я просто попытался его VERSION="16.04.2 LTS (Xenial Xerus)"в bashраковине, и последний echoраз печатает пустую строку. Какую версию Linux и какую оболочку вы используете? К сожалению, не может отобразить сеанс терминала в комментарии.
abalter
29

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

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
Лука Боррионе
источник
2
+1 Это полностью сработало для меня. Мне нужно было поместить несколько строк, разделенных новой строкой, в массив, и read -a arr <<< "$strings"не работал с IFS=$'\n'.
Стефан ван ден Аккер,
Это не совсем отвечает на оригинальный вопрос.
Майк
29

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

string='first line
        second line
        third line'

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

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

Или намного более простой readarray bash :

readarray -t lines <<<"$string"

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

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

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

источник
2
Хотя не каждое решение подходит для любой ситуации, ваше упоминание о readarray ... заменило мои последние два часа на 5 минут ... вы получили мой голос
Злой 84
7

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

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

Принты 1

ssanch
источник
1
это печатает 1 2 3 4 в моем случае
minigeek
6

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

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

Вы можете использовать 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 для регулярных выражений, и я также включил 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
##
Dawg
источник
Очень классное решение! Я никогда не думал об использовании цикла в совпадении с регулярным выражением, изящном использовании $BASH_REMATCH. Это работает, и действительно избегает порождения подоболочек. +1 от меня. Однако, в порядке критики, само регулярное выражение немного неидеально, так как кажется, что вы были вынуждены дублировать часть токена-разделителя (в частности, запятую), чтобы обойти отсутствие поддержки не жадных множителей (также lookarounds) в ERE («расширенный» вкус регулярных выражений, встроенный в bash). Это делает его немного менее универсальным и надежным.
bgoldst
Во-вторых, я провел несколько сравнительных тестов, и, хотя производительность лучше, чем у других решений для небольших строк, она экспоненциально ухудшается из-за повторного восстановления строк, становясь катастрофической для очень больших строк. Смотрите мое редактирование вашего ответа.
bgoldst
@bgoldst: Какой классный тест! В защиту регулярного выражения для 10 или 100 тысяч полей (что разделяет регулярное выражение), вероятно, будет какая-то форма записи (например, \nтекстовые строки с разделителями), включающая эти поля, так что катастрофическое замедление, скорее всего, не произойдет. Если у вас есть строка с 100 000 полей - возможно, Bash не идеален ;-) Спасибо за тест. Я узнал одну или две вещи.
Dawg
4

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

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

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

Если вы ищете решение проблемы многосимвольного разделителя, я предлагаю рассмотреть сообщение Малликарджуна М , в частности ответ от 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?

MrPotatoHead
источник
1
Смотрите мой комментарий для аналогичного, но улучшенного подхода.
xebeche
3

Это работает для меня на OSX:

string="1 2 3 4 5"
declare -a array=($string)

Если ваша строка имеет другой разделитель, просто 1-й замените их пробелом:

string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr "$delimiter" " "))

Просто :-)

К кра
источник
Работает как для Bash, так и для Zsh, что является плюсом!
Элайджа В. Ганье
2

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

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

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

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

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

Линдсей-потребности-сна
источник
2

Я наткнулся на этот пост, когда хотел разобрать входные данные, такие как: 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
balaganAtomi
источник
1

Попробуй это

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

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

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

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

Джефф Ли
источник
1

Мы можем использовать команду tr для разделения строки на объект массива. Работает как MacOS, так и Linux

  #!/usr/bin/env bash
  currentVersion="1.0.0.140"
  arrayData=($(echo $currentVersion | tr "." "\n"))
  len=${#arrayData[@]}
  for (( i=0; i<=$((len-1)); i++ )); do 
       echo "index $i - value ${arrayData[$i]}"
  done

Другой вариант использовать команду IFS

IFS='.' read -ra arrayData <<< "$currentVersion"
#It is the same as tr
arrayData=($(echo $currentVersion | tr "." "\n"))

#Print the split string
for i in "${arrayData[@]}"
do
    echo $i
done
sopheamak
источник
0

Использовать этот:

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

#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe
Эдуардо Куомо
источник
3
Плохо: возможно разделение слов и расширение пути. Пожалуйста, не возвращайте старые вопросы с хорошими ответами, чтобы дать плохие ответы.
gniourf_gniourf
2
Это может быть плохой ответ, но это все еще правильный ответ. Флаггеры / рецензенты: для неправильных ответов, таких как этот, downvote, не удаляйте!
Скотт Уэлдон
2
@gniourf_gniourf Не могли бы вы объяснить, почему это плохой ответ? Я действительно не понимаю, когда это терпит неудачу.
Георгий Советов
3
@GeorgeSovetov: Как я уже сказал, это может быть разделено на части и расширено. В более общем смысле , расщепление строки в массив , как array=( $string )это ( к сожалению , очень часто) антипаттерн: слово происходит расщепление: string='Prague, Czech Republic, Europe'; Происходит раскрытие пути: string='foo[abcd],bar[efgh]'произойдет сбой, если у вас есть файл с именем, например, foodили barfв вашем каталоге. Единственное допустимое использование такой конструкции - когда stringэто глоб.
gniourf_gniourf
0

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

С немного меньшей церемонией:

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

например

string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar
user1009908
источник
4
Eval это зло! не делай этого
Цезарсоль
1
Пфф. Нет. Если вы пишете сценарии, достаточно большие, чтобы это имело значение, вы делаете это неправильно. В коде приложения eval - это зло. В сценариях оболочки это распространено, необходимо и несущественно.
user1009908
2
вставьте $переменную в свою переменную, и вы увидите ... Я пишу много скриптов, и мне никогда не приходилось использовать одинeval
caesarsol
2
Вы правы, это можно использовать только тогда, когда известно, что ввод чистый. Не надежное решение.
user1009908 22.12.15
Единственный раз, когда мне приходилось использовать eval, было приложение, которое самостоятельно генерировало бы свой собственный код / ​​модули ... И это никогда не имело никакой формы пользовательского ввода ...
Сердитый 84
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.: /dba/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

Для многослойных элементов, почему бы не что-то вроде

$ array=($(echo -e $'a a\nb b' | tr ' ' '§')) && array=("${array[@]//§/ }") && echo "${array[@]/%/ INTERELEMENT}"

a a INTERELEMENT b b INTERELEMENT
Whimusical
источник
-1

Другой способ будет:

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

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

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

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

  1. Bash предоставляет встроенную функцию readarrayдля этой цели. Давайте использовать это.
  2. Избегайте уродливых и ненужных трюков, таких как изменение IFS, зацикливание, использованиеeval или добавление дополнительного элемента, а затем его удаление.
  3. Найдите простой, читаемый подход, который можно легко адаптировать к подобным проблемам.

readarrayКоманда простая в использовании с новой строкой в качестве разделителя. С другими разделителями это может добавить дополнительный элемент в массив. Самый чистый подход заключается в том, чтобы сначала адаптировать наш ввод в форму, которая хорошо работает сreadarray он прежде чем передать его.

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

string='  Paris,France  ,   All of Europe  '
readarray -t foo < <(tr ',' '\n' <<< "$string" |sed 's/^ *//' |sed 's/ *$//')
declare -p foo

# declare -a foo='([0]="Paris" [1]="France" [2]="All of Europe")'
Брайан Роуч
источник
-2

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

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

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

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