В сценарии Bash я хотел бы разделить строку на кусочки и сохранить их в массиве.
Линия:
Paris, France, Europe
Я хотел бы иметь их в таком массиве, как этот:
array[0] = Paris
array[1] = France
array[2] = Europe
Я хотел бы использовать простой код, скорость команды не имеет значения. Как я могу это сделать?
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}"
Большие отрицательные смещения выбирают дальше от конца массива. Обратите внимание на пробел перед знаком минус в более старой форме. Это требуется.
Все ответы на этот вопрос так или иначе неверны.
IFS=', ' read -r -a array <<< "$string"
1: Это неправильное использование $ IFS
. Значение переменной $ IFS
not принимается за один разделитель строк длины переменной , скорее это воспринимается как набор односимвольных строковых разделителей, где каждое поле, которое read
отделяется от входной строки, может быть прекращено символом any * в наборе (запятая или пространство, в этом примере).
На самом деле, для настоящих сторонников полное значение «$ IFS» немного более активно. Из руководства по ударам:
Оболочка обрабатывает каждый символ IFS как разделитель и разбивает результаты других расширений на слова, используя эти символы в качестве терминаторов полей. Если IFS не установлен, или его значение точно &л;пространство и;GT;&л;tab >&л;newline >, по умолчанию, затем последовательности &л;пространство и;GT;, &л;tab >, и &л;newline > в начале и в конце результаты предыдущих расширений игнорируются, и любая последовательность символов IFS , не в начале или конце, служит для разграничения слов. Если IFS имеет значение, отличное от значения по умолчанию, то последовательности пробелов & lt; space & gt; , & lt; tab > и & lt; newline > игнорируются в начале и конце слова, если символ пробела * в значении. Любой символ в IFS , который не является пробелом IFS , наряду с любыми соседними символами пробела IFS , разграничивает поле. Последовательность пробелов IFS также рассматривается как разделитель. Если значение IFS равно нулю, разделение слов не происходит.
В принципе, для ненулевых значений по умолчанию $ IFS
, поля можно разделить с помощью любого из них (1) последовательность из одного или нескольких символов, которые все из набора &"IFS пробелы символы &" ,(то есть, какой бы из &л;пространство и;GT;, &л;tab >, и &л;newline > (&"newline &" смысл [линия подачи (LF)https://en.wikipedia.org/wiki/Newline) присутствуют где угодно в $ IFS
) или (2) любой не- &"IFS пробел персонажа &" это 's присутствует в $ IFS
вместе с чем угодно &"IFS пробелы символы &" окружите его в строке ввода.
Для OP возможно, что второй режим разделения, который я описал в предыдущем абзаце, - это именно то, что он хочет для своей входной строки, но мы можем быть уверены, что первый описанный мною режим разделения неверен вообще. Например, что, если его входной строкой был «Лос-Анджелес, США, Северная Америка»?
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
, как мы делаем в этом примере с [здесь-строкой](https://www.gnu.org/software/bash/manual /html_node/Redirections.html#Here-Strings) механизм, и, таким образом, необработанный ввод гарантированно будет потерян. Код, который питает встроенный 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», как я продемонстрирую позже.
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'`)
Эти решения используют разделение слов в назначении массива, чтобы разделить строку на поля. Как ни странно, так же, как читать
, Общее разделение слов также использует специальную переменную $ IFS
, хотя в этом случае подразумевается, что оно установлено на значение по умолчанию &;л;пространство и;GT;&л;tab >&л;newline >, и, следовательно, любая последовательность из одного или нескольких символов IFS (которые все символы пробела сейчас) считается разделителем полей.
Это решает проблему двух уровней расщепления, совершаемых «read», поскольку расщепление слов само по себе составляет только один уровень расщепления. Но, как и прежде, проблема здесь в том, что отдельные поля во входной строке уже могут содержать символы $ IFS
, и, таким образом, они будут неправильно разделены во время операции разделения слов. Это не относится ни к одной из строк ввода выборки, предоставленных этими респондентами (насколько это удобно...), но, конечно, это не меняет того факта, что любая кодовая база, которая использовала эту идиому, могла бы тогда взорваться, если бы это предположение когда-либо было нарушено в какой-то момент вниз по линии. Еще раз рассмотрим мой контрпример «Лос-Анджелес, США, Северная Америка» (или «Лос-Анджелес: Соединенные Штаты: Северная Америка»).
Кроме того, за расщеплением слов обычно следует расширение имени файла ( aka расширение пути aka grobbing), который, если это сделано, потенциально может повредить слова, содержащие символы * , содержащие?
, или[
, за которым следует ]
(и, если заданоextglob
, скобленные фрагменты, предшествующие?
, *
, +
, @
или !
) сопоставляя их с объектами файловой системы и соответственно расширяя слова («глобы»). Первый из этих трех респондентов ловко подорвал эту проблему, запустив set -f
заранее, чтобы отключить глобирование. Технически это работает (хотя вы, вероятно, должны добавить set + f
впоследствии, чтобы снова включить globbing для последующего кода, который может зависеть от него), но нежелательно связываться с глобальными настройками оболочки, чтобы взломать базовую строку в массив операция синтаксического анализа в локальном коде.
Другая проблема с этим ответом заключается в том, что все пустые поля будут потеряны. Это может быть или не быть проблемой, в зависимости от приложения.
Примечание: если вы 'собираюсь использовать это решение, это 'лучше использовать ${строка //: / }
&"замена шаблона &" форма [расширение параметров]https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html#Shell-Parameter-Expansion) вместо того, чтобы идти на проблему вызова замены команды (который разветвляет оболочку) запуск трубопровода, и запуск внешнего исполняемого файла (tr
или sed
) поскольку расширение параметров является чисто внутренней операцией оболочки. (Кроме того, для решений tr
и sed
входная переменная должна быть дважды процитирована внутри замены команды; в противном случае разделение слов вступит в силу в команде echo
и потенциально может привести к путанице значений полей. Также $ (...)
форма замены команды предпочтительнее старой `...Форма
, поскольку она упрощает вложение замен команд и позволяет лучше выделять синтаксис текстовыми редакторами.)
str="a, b, c, d" # assuming there is a space after ',' as in Q
arr=(${str//,/}) # delete all occurrences of ','
Этот ответ почти такой же, как # 2 . Разница в том, что ответчик сделал предположение, что поля разделены двумя символами, один из которых представлен в стандарте $ IFS
, а другой нет. Он решил этот довольно специфический случай, удалив символ, не представленный в IFS, используя расширение подстановки шаблонов, а затем используя разделение слов, чтобы разделить поля на сохранившемся символе-разделителе, представленном IFS.
Это не очень общее решение. Кроме того, можно утверждать, что запятая действительно является символом «первичного» разделителя, и что разделять его, а затем в зависимости от пробела для разделения поля просто неправильно. Еще раз рассмотрите мой контрпример: «Лос-Анджелес, США, Северная Америка».
Кроме того, опять же, расширение имени файла может повредить расширенные слова, но это можно предотвратить, временно отключив globbing для назначения с помощью set -f
, а затем set + f
.
Кроме того, опять же, все пустые поля будут потеряны, что может быть или не быть проблемой в зависимости от приложения.
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';
.)
countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"
IFS=', ' eval 'array=($string)'
Это решение фактически является крестиком между # 1 (в том смысле, что оно устанавливает $ IFS
в запятое пространство) и # 2-4 (в том смысле, что оно использует разделение слов для разделения строки на поля). Из-за этого он страдает от большинства проблем, которые затрагивают все вышеперечисленные неправильные ответы, вроде как худший из всех миров.
Кроме того, что касается второго варианта, может показаться, что вызов eval
совершенно не нужен, поскольку его аргумент является литеральным строком с одним кавычками и, следовательно, статически известен. Но на самом деле использование «эвэ» таким образом имеет очень неочевидную выгоду. Обычно, когда вы запускаете простую команду, которая состоит из назначения переменной only , что означает, что после нее не следует фактическое командное слово, назначение вступает в силу в среде оболочки:
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
Соответствующая цитата из руководства по удалению:
Если имя команды не получено, назначения переменных влияют на текущую среду оболочки. В противном случае переменные добавляются в среду выполненной команды и не влияют на текущую среду оболочки.
Эту функцию назначения переменных можно использовать только временно, чтобы изменить $ IFS
, что позволяет нам избежать всего гамбита сохранения и восстановления, подобного тому, который выполняется с переменной $ OIFS
в первом варианте. Но проблема, с которой мы сталкиваемся здесь, заключается в том, что команда, которую нам нужно запустить, сама по себе является простым переменным заданием, и, следовательно, она не будет включать командное слово, чтобы сделать назначение $ IFS
временным. Вы можете подумать про себя, почему бы просто не добавить командное слово без операции к утверждению, как [: buildin
](https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell -Builtins.html#Bourne-Shell-Builtins), чтобы сделать назначение $ 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"; просто будьте осторожны, чтобы в одиночку процитировать строку аргументов, чтобы защитить от угроз безопасности.
Но опять же, из-за агломерации проблем «худшего из всех миров», это все еще неправильный ответ на требование ОП.
IFS=', '; array=(Paris, France, Europe)
IFS=' ';declare -a array=(Paris France Europe)
Um... что? OP имеет строковую переменную, которую необходимо проанализировать в массив. Этот «ответ» начинается с дословного содержимого входной строки, вставленной в литерал массива. Я думаю, это один из способов сделать это.
Похоже, что ответчик мог предположить, что переменная $ IFS
влияет на все синтаксические анализа bash во всех контекстах, что неверно. Из руководства по bash:
IFS & nbsp; & nbsp; & nbsp; & nbsp; Внутренний разделитель полей, который используется для разделения слов после расширения и для разделения строк на слова с помощью встроенной команды read . Значение по умолчанию: & lt; space > & lt; tab & gt; & lt; newline > .
Таким образом, специальная переменная $ IFS
фактически используется только в двух контекстах: (1) разделение слов, которое выполняется после расширения (что означает not при анализе исходного кода bash) и (2) для разделения входных строк на слова read
встроенный.
Позвольте мне попытаться прояснить это. Я думаю, что было бы хорошо провести различие между резким и исполнением . Bash должен сначала проанализировать исходный код, который, очевидно, является событием parsing , а затем он выполняет код, когда расширение входит в изображение. Расширение действительно событие выполнение . Кроме того, Я не согласен с описанием переменной $ IFS
, которую я только что процитировал выше; вместо того, чтобы говорить, что разделение слов выполняется после расширения , Я бы сказал, что разделение слов выполняется во время расширения, или, возможно, даже точнее, разделение слов является частью процесса расширения. Фраза «разделение слов» относится только к этому этапу расширения; его никогда не следует использовать для обозначения синтаксического анализа исходного кода bash, хотя, к сожалению, документы, кажется, часто разбрасывают слова «разделение» и «слова». Вот соответствующий отрывок из linux.die.net версии руководства по bash:
Расширение выполняется в командной строке после того, как оно было разделено на слова. Выполнено семь видов расширения: расширение brace , расширение tilde , расширение parameter и variable , замена команды , расширение арифметики , разделение слов и расширение пути .
Порядок расширений: расширение скобок; расширение тильды, расширение параметров и переменных, арифметическое расширение и замена команд (сделано слева направо); разделение слов; и расширение пути.
Вы можете утверждать, что версия GNU руководства работает немного лучше, поскольку оно выбирает слово «токены» вместо «слов» в первом предложении раздела «Расширение»:
Расширение выполняется в командной строке после его разделения на токены.
Важным моментом является то, что $ IFS
не меняет способ анализа исходного кода bash. Анализ исходного кода bash на самом деле является очень сложным процессом, который включает распознавание различных элементов грамматики оболочки, таких как последовательности команд, списки команд, конвейеры, расширения параметров, арифметические замены и замены команд. По большей части процесс синтаксического анализа bash не может быть изменен действиями на уровне пользователя, такими как назначения переменных (на самом деле, есть некоторые незначительные исключения из этого правила; например, см. Различные [compatxx
sell settings](https:// www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html # The-Shoptin, который. Верхние «слова» / «токены», которые являются результатом этого сложного процесса синтаксического анализа, затем расширяются в соответствии с общим процессом «расширения», как разбито в выдержках из документации выше, где разделение слов расширено (расширение?) текст в слова ниже по течению - это просто один шаг этого процесса. Разделение слов касается только текста, который был выплюнут из предыдущего шага расширения; это не влияет на буквальный текст, который был проанализирован сразу из исходного потока.
string='first line
second line
third line'
while read -r line; do lines+=("$line"); done <<<"$string"
Это одно из лучших решений. Обратите внимание, что мы вернулись к использованию 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")
Как и ожидалось, неучтенные окружающие пробелы были втянуты в значения полей, и, следовательно, это должно быть исправлено впоследствии с помощью операций обрезки (это также может быть сделано непосредственно во время цикла). Но есть еще одна очевидная ошибка: Европа отсутствует! Что с этим случилось? Ответ заключается в том, что read
возвращает сбойный код возврата, если он попадает в конец файла (в этом случае мы можем вызвать его в конце строки), не встречая окончательного терминатора поля в последнем поле. Это приводит к преждевременному разрыву цикла, и мы теряем последнее поле.
Технически эта же ошибка затронула и предыдущие примеры; разница в том, что разделитель полей был принят за LF, который по умолчанию, когда вы не 't укажите опцию -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) за один раз.
Итак, в целом, это довольно мощное решение. Остается только слабость - отсутствие поддержки многосимвольных разделителей, о чем я расскажу позже.
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')
Мы видим, что результат идентичен результату, который мы получили от двойного условного подхода к циклическому решению «читать», обсуждаемому в # 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) отсутствие поддержки многосимвольных разделителей.
Пробел, конечно, можно было бы обрезать позже (например, см. https://stackoverflow.com/questions/369758/how-to-trim-whitespace-from-a-bash-variable). Но если мы сможем взломать многосимвольный разделитель, то это решит обе проблемы за один выстрел.
К сожалению, нет прямого способа заставить работать многосимвольный разделитель. Лучшее решение, о котором я думал, - это предварительно обработать входную строку, чтобы заменить многосимвольный разделитель на односимвольный разделитель, который гарантированно не будет сталкиваться с содержимым входной строки. Единственный символ, который имеет эту гарантию, - это байт 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
. К сожалению, у меня не хватило места против сурового лимита поста Stack Overflow в 30 000 символов, поэтому я не смогу это объяснить. Я оставлю это как упражнение для читателя.
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")
Вот способ без установки 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. Таким образом, чтобы предотвратить расширение некоторых символов (например, *
), рекомендуется приостановить глобирование для этого сценария.
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"
Отпечатки три
Иногда мне случалось, что метод, описанный в принятом ответе, не работал, особенно если разделителем является возврат кареты.& Лт; br > В тех случаях я решил таким образом:
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
Принятый ответ работает для значений в одной строке.& Лт; br > Если переменная имеет несколько строк:
string='first line
second line
third line'
Нам нужна совсем другая команда, чтобы получить все строки:
при чтении -r строки; делать строки + = ("$ line"); сделано < < < "$ string"
Или гораздо более простой bash readarray :
readarray -t lines <<<"$string"
Печатать все линии очень легко, используя функцию printf:
printf ">[%s]\n" "${lines[@]}"
>[first line]
>[ second line]
>[ third line]
Ключом к разделению вашей строки на массив является многосимвольный разделитель ", "
. Любое решение, использующее 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
с решением regex 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
##
Это похоже на подход Jmoney38, но с использованием sed:
string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
echo ${array[0]}
Отпечатки 1
Чистое решение для мультисимвольного разделителя 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?
Попробуй это
IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done
Это просто. Если вы хотите, вы также можете добавить объявление (а также удалить запятые):
IFS=' ';declare -a array=(Paris France Europe)
IFS добавляется для отмены вышеизложенного, но он работает без него в новом экземпляре bash
Я сталкивался с этим постом, когда искал анализ ввода, как: 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
Это работает для меня на OSX:
string = "1 2 3 4 5"
объявлять -a array = ($ string)
Если ваша строка имеет другой разделитель, просто 1-й замените их пробелом:
string = "1,2,3,4,5"
delimiter = ","
объявлять -a array = ($ (echo $ string | tr "$ delimiter" ""))
Просто :-)
Еще один способ сделать это без изменения IFS:
read -r -a myarray <<< "${string//, /$IFS}"
Вместо того, чтобы изменять IFS в соответствии с нашим желаемым разделителем, мы можем заменить все вхождения нашего желаемого разделителя ", "
содержимым $ IFS
через"$ {string //, / $ IFS}"
.
Может быть, это будет медленно для очень больших строк, хотя?
Это основано на ответе Денниса Уильямсона.
Мы можем использовать команду 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
ОБНОВЛЕНИЕ: Не делайте этого из-за проблем с eval.
С чуть меньшей церемонией:
IFS=', ' eval 'array=($string)'
например.
string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar
Используйте это:
countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"
#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe
Вот мой хак!
Разделение строк на строки - довольно скучная вещь, которую нужно делать с помощью bash. Что происходит, так это то, что у нас ограниченные подходы, которые работают только в нескольких случаях (разделенные на «;», «/», «."и так далее) или у нас есть различные побочные эффекты в выходах.
Подход, приведенный ниже, потребовал ряда маневров, но я считаю, что он будет работать для большинства наших нужд!
#!/bin/bash
# --------------------------------------
# SPLIT FUNCTION
# ----------------
F_SPLIT_R=()
f_split() {
: 'It does a "split" into a given string and returns an array.
Args:
TARGET_P (str): Target string to "split".
DELIMITER_P (Optional[str]): Delimiter used to "split". If not
informed the split will be done by spaces.
Returns:
F_SPLIT_R (array): Array with the provided string separated by the
informed delimiter.
'
F_SPLIT_R=()
TARGET_P=$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
Другой способ будет:
string="Paris, France, Europe"
IFS=', ' arr=(${string})
Теперь ваши элементы хранятся в массиве "arr". Итерация через элементы:
for i in ${arr[@]}; do echo $i; done
Другой подход может быть:
str="a, b, c, d" # assuming there is a space after ',' as in Q
arr=(${str//,/}) # delete all occurrences of ','
После этого «arr» - массив с четырьмя строками. Это не требует работы с IFS или чтения или другими специальными вещами, следовательно, гораздо проще и прямолинейнее.