Как разделить строку на разделитель в Bash?


2044

У меня есть эта строка хранится в переменной:

IN="bla@some.com;john@home.com"

Теперь я хотел бы разделить строки по ;разделителю, чтобы у меня было:

ADDR1="bla@some.com"
ADDR2="john@home.com"

Я не обязательно нужен ADDR1и ADDR2переменные. Если они являются элементами массива, это даже лучше.


После предложений из ответов, приведенных ниже, я получил следующее:

#!/usr/bin/env bash

IN="bla@some.com;john@home.com"

mails=$(echo $IN | tr ";" "\n")

for addr in $mails
do
    echo "> [$addr]"
done

Вывод:

> [bla@some.com]
> [john@home.com]

Было решение, включающее установку Internal_field_separator (IFS) в ;. Я не уверен, что случилось с этим ответом, как вы IFSвернетесь к настройкам по умолчанию?

RE: IFSрешение, я попробовал это, и оно работает, я сохраняю старое IFSи затем восстанавливаю это:

IN="bla@some.com;john@home.com"

OIFS=$IFS
IFS=';'
mails2=$IN
for x in $mails2
do
    echo "> [$x]"
done

IFS=$OIFS

Кстати, когда я пытался

mails2=($IN)

Я получил только первую строку при печати в цикле, без скобок вокруг $INэто работает.


14
Что касается вашего «Edit2»: вы можете просто «сбросить IFS», и он вернется в состояние по умолчанию. Нет необходимости сохранять и восстанавливать его явно, если только у вас нет причин ожидать, что для него уже установлено значение не по умолчанию. Более того, если вы делаете это внутри функции (а если нет, то почему бы и нет?), Вы можете установить IFS в качестве локальной переменной, и он вернется к своему предыдущему значению после выхода из функции.
Брукс Моисей

19
@BrooksMoses: (a) +1 за использование, local IFS=...где это возможно; (b) -1 для unset IFS, это не совсем сбрасывает IFS к его значению по умолчанию, хотя я считаю, что неустановленный IFS ведет себя так же, как значение по умолчанию IFS ($ '\ t \ n'), однако это кажется плохой практикой слепо предполагать, что ваш код никогда не будет вызываться с установленным значением IFS; (c) другая идея состоит в том, чтобы вызывать подоболочку: (IFS=$custom; ...)при выходе из подоболочки IFS вернется к тому, что было изначально.
dubiousjim

Я просто хочу быстро взглянуть на пути, чтобы решить, куда бросить исполняемый файл, поэтому я прибег к запуску ruby -e "puts ENV.fetch('PATH').split(':')". Если вы хотите остаться чистым, bash не поможет, но проще использовать любой язык сценариев со встроенным разделением.
nicooga

4
for x in $(IFS=';';echo $IN); do echo "> [$x]"; done
user2037659

2
Чтобы сохранить его в виде массива, я должен был поставить еще один набор скобок и изменить его \nтолько на пробел. Итак, последняя строка mails=($(echo $IN | tr ";" " ")). Так что теперь я могу проверять элементы mails, используя нотацию массива mails[index]или просто повторяя в цикле
afranques

Ответы:


1237

Вы можете установить переменную внутреннего разделителя полей (IFS), а затем разрешить ее анализ в массив. Когда это происходит в команде, тогда присваивание IFSпроисходит только среде этой отдельной команды (to read). Затем он анализирует входные данные в соответствии со IFSзначением переменной в массив, который мы затем можем перебрать.

IFS=';' read -ra ADDR <<< "$IN"
for i in "${ADDR[@]}"; do
    # process "$i"
done

Он будет анализировать одну строку элементов, разделенных ;, помещая ее в массив. Материал для обработки всего $IN, каждый раз одна строка ввода разделяется ;:

 while IFS=';' read -ra ADDR; do
      for i in "${ADDR[@]}"; do
          # process "$i"
      done
 done <<< "$IN"

22
Это, наверное, лучший способ. Как долго IFS сохранит свое текущее значение, сможет ли он испортить мой код, будучи установленным, когда его не должно быть, и как я могу сбросить его, когда я закончу с этим?
Крис Латс

7
теперь, после исправления, только в течение срока действия команды чтения :)
Johannes Schaub - litb

14
Вы можете прочитать все сразу, не используя цикл while: read -r -d '' -a addr <<< "$ in" # Ключ -d '' здесь ключевой, он сообщает read не останавливаться на первой новой строке ( который является значением по умолчанию -d), но продолжается до EOF или NULL-байта (которые встречаются только в двоичных данных).
lhunath

56
@LucaBorrione Установка IFSв той же строке, что и readбез точки с запятой или другого разделителя, в отличие от отдельной команды, ограничивает ее этой командой, поэтому она всегда «восстанавливается»; вам не нужно ничего делать вручную.
Чарльз Даффи

5
@imagineerThis Существует ошибка, связанная с приведенными здесь строками и локальными изменениями в IFS, которые необходимо $INзаключать в кавычки. Ошибка исправлена ​​в bash4.3.
Chepner

973

Взято из скриптового массива Bash :

IN="bla@some.com;john@home.com"
arrIN=(${IN//;/ })

Объяснение:

Эта конструкция заменяет все вхождения ';'(начальная //означает глобальную замену) в строке INна ' '(один пробел), а затем интерпретирует строку, разделенную пробелом, как массив (это то, что делают окружающие скобки).

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

Есть несколько распространенных ошибок:

  1. Если в исходной строке есть пробелы, вам нужно использовать IFS :
    • IFS=':'; arrIN=($IN); unset IFS;
  2. Если в исходной строке есть пробелы и разделителем является новая строка, вы можете установить IFS с помощью:
    • IFS=$'\n'; arrIN=($IN); unset IFS;

84
Я просто хочу добавить: это самый простой из всех, вы можете получить доступ к элементам массива с помощью $ {arrIN [1]} (конечно, начиная с нулей)
Oz123

26
Нашел его: техника изменения переменной внутри $ {} известна как «расширение параметров».
KomodoDave

23
Нет, я не думаю, что это работает, когда присутствуют также пробелы ... это преобразование ',' в '', а затем построение массива, разделенного пробелами.
Итан

12
Очень краткий, но есть предостережения для общего использования : оболочка применяет разбиение слов и расширения к строке, что может быть нежелательным; просто попробуйте. IN="bla@some.com;john@home.com;*;broken apart", Вкратце: этот подход сломается, если ваши токены содержат встроенные пробелы и / или символы. например *, чтобы токен совпадал с именами файлов в текущей папке.
mklement0

53
Это плохой подход по другим причинам: например, если ваша строка содержит ;*;, то *он будет расширен до списка имен файлов в текущем каталоге. -1
Чарльз Даффи

249

Если вы не возражаете обработать их немедленно, мне нравится делать это:

for i in $(echo $IN | tr ";" "\n")
do
  # process
done

Вы можете использовать этот тип цикла для инициализации массива, но, вероятно, есть более простой способ сделать это. Надеюсь, это поможет, хотя.


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

Понимаю. Да, я нахожу себя в этих глупых экспериментах, я буду учиться чему-то новому каждый раз, когда пытаюсь что-то ответить. Я отредактировал материал, основанный на обратной связи #bash IRC и восстановленный :)
Йоханнес Шауб - litb

33
-1, вы явно не знаете о разбиении слов, потому что это приводит к двум ошибкам в вашем коде. один - когда вы не заключаете в кавычки $ IN, а другой - когда вы притворяетесь, что новая строка - единственный разделитель, используемый в разбиении слов. Вы перебираете каждое WORD в IN, а не каждую строку, и ОПРЕДЕЛЕННО не каждый элемент, разделенный точкой с запятой, хотя может показаться, что побочный эффект выглядит так, как будто он работает.
lhunath

3
Вы можете изменить его на «$ IN» | tr ';' '\ n' | пока читаешь -r ADDY; do # process "$ ADDY"; Я думаю, что это сделано для того, чтобы ему повезло. Обратите внимание, что это приведет к развороту, и вы не сможете изменить внешние переменные внутри цикла (поэтому я использовал синтаксис <<< "$ IN"), а затем
Йоханнес Шауб - litb

8
Подводя итоги обсуждения в комментариях: Предостережения для общего пользования : оболочка применяет разбиение слов и расширения к строке, что может быть нежелательным; просто попробуйте. IN="bla@some.com;john@home.com;*;broken apart", Вкратце: этот подход сломается, если ваши токены содержат встроенные пробелы и / или символы. например *, чтобы токен совпадал с именами файлов в текущей папке.
mklement0

202

Совместимый ответ

Есть много разных способов сделать это в ,

Тем не менее, важно сначала отметить, что bashимеет много специальных функций (так называемых bashisms ), которые не будут работать ни в одном другом,

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

Например: на моем Debian GNU / Linux есть стандартная оболочка под названием; Я знаю многих людей, которые любят использовать другую оболочку под названием; и есть также специальный инструмент под названием с его собственным интерпретатором оболочки ().

Запрашиваемая строка

Строка, которая будет разбита в приведенном выше вопросе:

IN="bla@some.com;john@home.com"

Я буду использовать модифицированную версию этой строки, чтобы убедиться, что мое решение устойчиво к строкам, содержащим пробелы, которые могут нарушить другие решения:

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"

Разделить строку на основе разделителя в (версия> = 4.2)

В чистом виде bash мы можем создать массив с элементами, разделенными временным значением для IFS ( разделитель входного поля ). IFS, помимо прочего, сообщает, bashкакой символ (символы) он должен рассматривать как разделитель между элементами при определении массива:

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"

# save original IFS value so we can restore it later
oIFS="$IFS"
IFS=";"
declare -a fields=($IN)
IFS="$oIFS"
unset oIFS

В более новых версиях bash, предварив команду с определением МФСА изменяет IFS для этой команды только и сбрасывает его в предыдущее значение сразу же после этого. Это означает, что мы можем сделать выше всего одну строку:

IFS=\; read -a fields <<<"$IN"
# after this command, the IFS resets back to its previous value (here, the default):
set | grep ^IFS=
# IFS=$' \t\n'

Мы можем видеть, что строка INбыла сохранена в массив с именем fields, разделенный на точки с запятой:

set | grep ^fields=\\\|^IN=
# fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name <fulnam@other.org>")
# IN='bla@some.com;john@home.com;Full Name <fulnam@other.org>'

(Мы также можем отобразить содержимое этих переменных, используя declare -p:)

declare -p IN fields
# declare -- IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
# declare -a fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name <fulnam@other.org>")

Обратите внимание, что readэто самый быстрый способ сделать разделение, потому что нет никаких вызванных вилок или внешних ресурсов.

Как только массив определен, вы можете использовать простой цикл для обработки каждого поля (или, вернее, каждого элемента в массиве, который вы сейчас определили):

# `"${fields[@]}"` expands to return every element of `fields` array as a separate argument
for x in "${fields[@]}" ;do
    echo "> [$x]"
    done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Или вы можете удалить каждое поле из массива после обработки, используя подход смещения , который мне нравится:

while [ "$fields" ] ;do
    echo "> [$fields]"
    # slice the array 
    fields=("${fields[@]:1}")
    done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

И если вам нужна простая распечатка массива, вам даже не нужно зацикливаться на нем:

printf "> [%s]\n" "${fields[@]}"
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Обновление: недавно > = 4.4

В новых версиях bashвы также можете играть с командой mapfile:

mapfile -td \; fields < <(printf "%s\0" "$IN")

Этот синтаксис сохраняет специальные символы, новые строки и пустые поля!

Если вы не хотите включать пустые поля, вы можете сделать следующее:

mapfile -td \; fields <<<"$IN"
fields=("${fields[@]%$'\n'}")   # drop '\n' added by '<<<'

С помощью mapfileвы также можете пропустить объявление массива и неявно «зацикливаться» на элементах с разделителями, вызывая функцию для каждого:

myPubliMail() {
    printf "Seq: %6d: Sending mail to '%s'..." $1 "$2"
    # mail -s "This is not a spam..." "$2" </path/to/body
    printf "\e[3D, done.\n"
}

mapfile < <(printf "%s\0" "$IN") -td \; -c 1 -C myPubliMail

(Примечание: \0конец строки формата бесполезен, если вам не нужны пустые поля в конце строки или они отсутствуют.)

mapfile < <(echo -n "$IN") -td \; -c 1 -C myPubliMail

# Seq:      0: Sending mail to 'bla@some.com', done.
# Seq:      1: Sending mail to 'john@home.com', done.
# Seq:      2: Sending mail to 'Full Name <fulnam@other.org>', done.

Или вы можете использовать <<<, и в теле функции включить некоторую обработку для удаления новой строки:

myPubliMail() {
    local seq=$1 dest="${2%$'\n'}"
    printf "Seq: %6d: Sending mail to '%s'..." $seq "$dest"
    # mail -s "This is not a spam..." "$dest" </path/to/body
    printf "\e[3D, done.\n"
}

mapfile <<<"$IN" -td \; -c 1 -C myPubliMail

# Renders the same output:
# Seq:      0: Sending mail to 'bla@some.com', done.
# Seq:      1: Sending mail to 'john@home.com', done.
# Seq:      2: Sending mail to 'Full Name <fulnam@other.org>', done.

Разделить строку на основе разделителя в

Если вы не можете использовать bashили если вы хотите написать что-то, что можно использовать во многих различных оболочках, вы часто не можете использовать bashisms - и это включает в себя массивы, которые мы использовали в решениях выше.

Однако нам не нужно использовать массивы для циклического перебора «элементов» строки. Существует синтаксис, используемый во многих оболочках для удаления подстрок строки из первого или последнего вхождения шаблона. Обратите внимание, что *подстановочный знак обозначает ноль или более символов:

(Отсутствие такого подхода в любом опубликованном решении является основной причиной, по которой я пишу этот ответ;)

${var#*SubStr}  # drops substring from start of string up to first occurrence of `SubStr`
${var##*SubStr} # drops substring from start of string up to last occurrence of `SubStr`
${var%SubStr*}  # drops substring from last occurrence of `SubStr` to end of string
${var%%SubStr*} # drops substring from first occurrence of `SubStr` to end of string

Как объяснил Score_Under :

#и %удалите максимально короткую подходящую подстроку из начала и конца строки соответственно, и

##и %%удалите максимально длинную подходящую подстроку.

Используя приведенный выше синтаксис, мы можем создать подход, в котором мы извлекаем «элементы» подстроки из строки, удаляя подстроки до или после разделителя.

Кодовый блок ниже хорошо работает в (включая Mac OS bash),, , а также «s :

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
while [ "$IN" ] ;do
    # extract the substring from start of string up to delimiter.
    # this is the first "element" of the string.
    iter=${IN%%;*}
    echo "> [$iter]"
    # if there's only one element left, set `IN` to an empty string.
    # this causes us to exit this `while` loop.
    # else, we delete the first "element" of the string from IN, and move onto the next.
    [ "$IN" = "$iter" ] && \
        IN='' || \
        IN="${IN#*;}"
  done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Радоваться, веселиться!


15
В #, ##, %и %%замены есть то , что ИМО проще объяснение , чтобы помнить (за сколько они удалить) #и %удалить кратчайшую строку соответствия, а также ##и %%удалить самое длинные возможное.
Score_Under

1
IFS=\; read -a fields <<<"$var"Терпит неудачу на переводы строк и добавить символ новой строки. Другое решение удаляет завершающее пустое поле.
Исаак

Разделитель оболочки - самый элегантный ответ, точка.
Эрик Чен,

Может ли последняя альтернатива использоваться со списком разделителей полей, установленным где-то еще? Например, я имею в виду использовать его в качестве сценария оболочки и передавать список разделителей полей в качестве позиционного параметра.
sancho.s ReinstateMonicaCellio

Да, в цикле:for sep in "#" "ł" "@" ; do ... var="${var#*$sep}" ...
Ф. Хаури

184

Я видел пару ответов со ссылкой на cutкоманду, но все они были удалены. Немного странно, что никто не уточнил это, потому что я думаю, что это одна из наиболее полезных команд для такого типа вещей, особенно для анализа файлов журнала с разделителями.

В случае разбиения этого конкретного примера на массив сценариев bash, trвозможно, он более эффективен, но cutего можно использовать, и он более эффективен, если вы хотите извлечь определенные поля из середины.

Пример:

$ echo "bla@some.com;john@home.com" | cut -d ";" -f 1
bla@some.com
$ echo "bla@some.com;john@home.com" | cut -d ";" -f 2
john@home.com

Очевидно, вы можете поместить это в цикл и выполнить итерацию параметра -f для независимого извлечения каждого поля.

Это становится более полезным, когда у вас есть лог-файл с разделителями со строками вроде этого:

2015-04-27|12345|some action|an attribute|meta data

cutочень удобно иметь возможность catэтого файла и выбрать конкретное поле для дальнейшей обработки.


6
Слава за использование cut, это правильный инструмент для работы! Многое очищено, чем любой из этих хакерских оболочек.
Мистер Мияги

4
Этот подход будет работать, только если вы заранее знаете количество элементов; вам нужно запрограммировать больше логики вокруг него. Он также запускает внешний инструмент для каждого элемента.
uli42

Точно, что я искал, пытаясь избежать пустой строки в CSV. Теперь я могу указать точное значение столбца. Работа с IFS уже используется в цикле. Лучше, чем ожидалось для моей ситуации.
Луи Лоудог Тротье

Очень полезно для извлечения идентификаторов и PID тоже, то есть
Милош

Этот ответ стоит прокрутить вниз на полстраницы :)
Gucu112

124

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

string="1;2"
echo $string | cut -d';' -f1 # output is 1
echo $string | cut -d';' -f2 # output is 2

1
Хотя он работает только с одним символом-разделителем, это то, что ищет OP (записи, разделенные точкой с запятой).
GuyPaddock

Отвечено около четырех лет назад @Ashok , а также более года назад @DougW , чем ваш ответ, с еще большей информацией. Пожалуйста, опубликуйте другое решение, чем у других.
MAChitgarha

90

Как насчет этого подхода:

IN="bla@some.com;john@home.com" 
set -- "$IN" 
IFS=";"; declare -a Array=($*) 
echo "${Array[@]}" 
echo "${Array[0]}" 
echo "${Array[1]}" 

Источник


7
+1 ... но я бы не назвал переменную "Array" ... пэт пев, наверное. Хорошее решение
Измир Рамирес

14
+1 ... но "установить" и объявить -a не нужны. Вы могли бы, а использовали толькоIFS";" && Array=($IN)
ата

+1 Только примечание: не рекомендуется ли сохранить старый IFS, а затем восстановить его? (как показывает stefanB в его edit3) люди, приземляющиеся здесь (иногда просто копирующие и вставляющие решение), могут не думать об этом
Luca Borrione

6
-1: во-первых, @ata прав, что большинство команд в этом ничего не делают. Во-вторых, он использует расщепление слов для формирования массива и не делает ничего, чтобы запретить глобальное расширение при этом (поэтому, если у вас есть символы глобуса в любом из элементов массива, эти элементы заменяются соответствующими именами файлов).
Чарльз Даффи

1
Предлагайте использованию $'...': IN=$'bla@some.com;john@home.com;bet <d@\ns* kl.com>'. Затем echo "${Array[2]}"напечатает строку с новой строкой. set -- "$IN"также необходимо в этом случае. Да, чтобы предотвратить глобальное расширение, решение должно включать set -f.
John_West

79

Я думаю, что AWK - лучшая и эффективная команда для решения вашей проблемы. AWK включен по умолчанию почти во все дистрибутивы Linux.

echo "bla@some.com;john@home.com" | awk -F';' '{print $1,$2}'

дам

bla@some.com john@home.com

Конечно, вы можете сохранить каждый адрес электронной почты, переопределив поле печати awk.


3
Или еще проще: echo "bla@some.com; john@home.com" | awk 'BEGIN {RS = ";"} {print}'
Джаро

@Jaro Это отлично сработало, когда у меня была строка с запятыми, и мне нужно было переформатировать ее в строки. Спасибо.
Акварель

В этом сценарии это сработало -> "echo" $ SPLIT_0 "| awk -F 'inode =' '{print $ 1}'"! У меня возникли проблемы при попытке использовать символы ("inode =") вместо символов (";"). $ 1, $ 2, $ 3, $ 4 устанавливаются как позиции в массиве! Если есть способ установить массив ... лучше! Спасибо!
Эдуардо Лусио

@EduardoLucio, что я думаю о том , может быть , вы можете сначала заменить разделитель inode=в ;, например , с помощью sed -i 's/inode\=/\;/g' your_file_to_process, а затем определить , -F';'когда применять awk, надежду на то, что может помочь вам.
Тонг

66
echo "bla@some.com;john@home.com" | sed -e 's/;/\n/g'
bla@some.com
john@home.com

4
-1 что если строка содержит пробелы? например IN="this is first line; this is second line" arrIN=( $( echo "$IN" | sed -e 's/;/\n/g' ) ), в этом случае будет получен массив из 8 элементов (элемент для каждого разделенного пробелом слова), а не 2 (элемент для каждой строки, разделенной
точкой с запятой

3
@ Luca Нет, скрипт sed создает ровно две строки. То, что создает несколько записей для вас, - это когда вы помещаете их в массив bash (который по умолчанию разделяется на пробел)
Лотария

В том-то и дело: OP должен хранить записи в массиве, чтобы зацикливаться на нем, как вы можете видеть в его правках. Я думаю, что ваш (хороший) ответ упущен, чтобы упомянуть, чтобы использовать его arrIN=( $( echo "$IN" | sed -e 's/;/\n/g' ) )для достижения этой цели, а также посоветовать изменить IFS IFS=$'\n'для тех, кто приземлится здесь в будущем и должен разбить строку, содержащую пробелы. (и восстановить его потом). :)
Лука Боррионе

1
@ Лука Хороший вопрос. Однако, когда я написал этот ответ, назначения массива не было в первоначальном вопросе.
лотерея

65

Это также работает:

IN="bla@some.com;john@home.com"
echo ADD1=`echo $IN | cut -d \; -f 1`
echo ADD2=`echo $IN | cut -d \; -f 2`

Будьте осторожны, это решение не всегда правильно. Если вы передадите только «bla@some.com», он назначит его как ADD1, так и ADD2.


1
Вы можете использовать -s, чтобы избежать упомянутой проблемы: superuser.com/questions/896800/… "-f, --fields = LIST выбрать только эти поля; также вывести любую строку, которая не содержит символа-разделителя, если опция -s не уточняется "
fersarr

34

Другой ответ на ответ Даррона , вот как я это делаю:

IN="bla@some.com;john@home.com"
read ADDR1 ADDR2 <<<$(IFS=";"; echo $IN)

Я думаю, что это так! Запустите приведенные выше команды, а затем «echo $ ADDR1 ... $ ADDR2», и я получу вывод «bla@some.com ... john@home.com»
nickjb

1
Это работает очень хорошо для меня ... Я использовал его, чтобы перебрать массив строк, который содержал разделенные запятыми данные DB, SERVER, PORT для использования mysqldump.
Ник

5
Диагноз: IFS=";"назначение существует только в $(...; echo $IN)подоболочке; Вот почему некоторые читатели (включая меня) изначально думают, что это не сработает. Я предположил, что весь $ IN был подбит ADDR1. Но ник это правильно; это работает. Причина в том, что echo $INкоманда анализирует свои аргументы, используя текущее значение $ IFS, но затем выводит их на стандартный вывод, используя разделитель пробелов, независимо от значения параметра $ IFS. Таким образом, чистый эффект такой, как если бы он звонил read ADDR1 ADDR2 <<< "bla@some.com john@home.com"(обратите внимание, что ввод не разделен пробелом; -отделен).
dubiousjim

1
Это не будет работать на пространствах и переводы строк, а также расширить символы *в echo $INс расширением некотируемого переменной.
Исаак

Мне очень нравится это решение. Описание того, почему это работает, было бы очень полезно и сделало бы его лучший общий ответ.
Майкл Гаскилл

32

В Bash, пуленепробиваемый способ, который будет работать, даже если ваша переменная содержит символы новой строки:

IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")

Смотреть:

$ in=$'one;two three;*;there is\na newline\nin this field'
$ IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")
$ declare -p array
declare -a array='([0]="one" [1]="two three" [2]="*" [3]="there is
a newline
in this field")'

Хитрость для этого заключается в том, чтобы использовать -dопцию read(разделитель) с пустым разделителем, так что readон вынужден читать все, что ему дают. И мы добавляем readименно содержимое переменной in, без завершающей строки, благодаря printf. Обратите внимание, что мы также добавляем разделитель, printfчтобы убедиться, что переданная строка readимеет конечный разделитель. Без него readобрезал бы потенциальные конечные пустые поля:

$ in='one;two;three;'    # there's an empty field
$ IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")
$ declare -p array
declare -a array='([0]="one" [1]="two" [2]="three" [3]="")'

конечное пустое поле сохраняется.


Обновление для Bash≥4.4

Начиная с Bash 4.4, встроенная функция mapfile(aka readarray) поддерживает -dвозможность указания разделителя. Отсюда и другой канонический способ:

mapfile -d ';' -t array < <(printf '%s;' "$in")

5
Я нашел его как редкое решение в этом списке, которое правильно работает с \nпробелами и *одновременно. Также нет петель; Переменная массива доступна в оболочке после выполнения (в отличие от ответа с наибольшим количеством голосов). Обратите внимание, in=$'...'что он не работает с двойными кавычками. Я думаю, что нужно больше голосов.
John_West

28

Как насчет этого одного лайнера, если вы не используете массивы:

IFS=';' read ADDR1 ADDR2 <<<$IN

Попробуйте использовать, read -r ...чтобы, например, два символа «\ t» во входных данных оказались одинаковыми двумя символами в ваших переменных (вместо одного символа табуляции).
dubiousjim

-1 Это не работает здесь (Ubuntu 12.04). Добавление echo "ADDR1 $ADDR1"\n echo "ADDR2 $ADDR2"к вашему фрагменту приведет к выводу ADDR1 bla@some.com john@home.com\nADDR2(\ n -
перевод

Вероятно, это связано с ошибкой, включающей IFSи здесь строки, которые были исправлены в bash4.3. Цитирование $INдолжно это исправить. (Теоретически, $INне подлежит разделению или смещению слова после его расширения, а это означает, что кавычки не должны быть необходимыми. Однако даже в 4.3 остается хотя бы одна ошибка - сообщается и планируется исправить - поэтому цитирование остается хорошим идея.)
Chepner

Это ломается, если $ in содержит символы новой строки, даже если $ IN указан в кавычках. И добавляет завершающий перевод строки.
Исаак

Проблема с этим и многими другими решениями также заключается в том, что предполагается, что в $ IN - ТОЛЬКО ДВУХ элементов, и что вы хотите, чтобы второй и последующие элементы были объединены в ADDR2. Я понимаю, что это отвечает требованиям, но это бомба замедленного действия.
Стивен легко

23

Без настройки IFS

Если у вас есть только двоеточие, вы можете сделать это:

a="foo:bar"
b=${a%:*}
c=${a##*:}

ты получишь:

b = foo
c = bar

20

Вот чистый 3-х вкладыш:

in="foo@bar;bizz@buzz;fizz@buzz;buzz@woof"
IFS=';' list=($in)
for item in "${list[@]}"; do echo $item; done

где IFSслова разграничиваются на основе разделителя и ()используются для создания массива . затем[@] используется для возврата каждого элемента как отдельного слова.

Если у вас есть какой-либо код после этого, вам также необходимо восстановить $IFS, например unset IFS.


5
Использование без $inкавычек позволяет расширять символы подстановки.
Исаак

10

Следующая функция Bash / zsh разделяет свой первый аргумент на разделитель, заданный вторым аргументом:

split() {
    local string="$1"
    local delimiter="$2"
    if [ -n "$string" ]; then
        local part
        while read -d "$delimiter" part; do
            echo $part
        done <<< "$string"
        echo $part
    fi
}

Например, команда

$ split 'a;b;c' ';'

доходность

a
b
c

Этот вывод может, например, передаваться другим командам. Пример:

$ split 'a;b;c' ';' | cat -n
1   a
2   b
3   c

По сравнению с другими решениями, данное имеет следующие преимущества:

  • IFSне переопределяется: из-за динамического определения области действия даже локальных переменных переопределение IFSпо циклу вызывает утечку нового значения в вызовы функций, выполняемые из цикла.

  • Массивы не используются: для чтения строки в массив readнеобходимо использовать флаг -aв Bash и -Azsh.

При желании функция может быть помещена в скрипт следующим образом:

#!/usr/bin/env bash

split() {
    # ...
}

split "$@"

Кажется, не работает с разделителями длиннее 1 символа: split = $ (split "$ content" "file: //")
madprops

Правда - от help read:-d delim continue until the first character of DELIM is read, rather than newline
Галле

8

Вы можете применить awk во многих ситуациях

echo "bla@some.com;john@home.com"|awk -F';' '{printf "%s\n%s\n", $1, $2}'

также вы можете использовать это

echo "bla@some.com;john@home.com"|awk -F';' '{print $1,$2}' OFS="\n"

7

Существует простой и умный способ, как это:

echo "add:sfff" | xargs -d: -i  echo {}

Но вы должны использовать gnu xargs, BSD xargs не может поддерживать -d delim. Если вы используете Apple Mac, как я. Вы можете установить GNU XARGS:

brew install findutils

тогда

echo "add:sfff" | gxargs -d: -i  echo {}

4

Это самый простой способ сделать это.

spo='one;two;three'
OIFS=$IFS
IFS=';'
spo_array=($spo)
IFS=$OIFS
echo ${spo_array[*]}

4

Здесь есть несколько классных ответов (errator esp.), Но для чего-то аналогичного разделению на другие языки - что я и имел в виду в первоначальном вопросе - я остановился на этом:

IN="bla@some.com;john@home.com"
declare -a a="(${IN/;/ })";

Теперь ${a[0]}и ${a[1]}т. Д., Как и следовало ожидать. Используйте ${#a[*]}для ряда условий. Или, конечно, повторить:

for i in ${a[*]}; do echo $i; done

ВАЖНАЯ ЗАМЕТКА:

Это работает в тех случаях, когда нет места для беспокойства, что решило мою проблему, но не может решить вашу. Перейти с $IFSрешением (ями) в этом случае.


Не работает, если INсодержит более двух адресов электронной почты. Пожалуйста, обратитесь к той же идее (но исправлено) в ответе Палиндрома
olibre

Лучше использовать ${IN//;/ }(двойной слеш), чтобы он также работал с более чем двумя значениями. Помните, что любой подстановочный знак ( *?[) будет расширен. И конечное пустое поле будет отброшено.
Исаак

3
IN="bla@some.com;john@home.com"
IFS=';'
read -a IN_arr <<< "${IN}"
for entry in "${IN_arr[@]}"
do
    echo $entry
done

Вывод

bla@some.com
john@home.com

Система: Ubuntu 12.04.1


IFS не устанавливается в конкретном контексте readздесь и, следовательно, может расстроить остальную часть кода, если таковой имеется.
Codeforester

2

Если нет места, почему бы не это?

IN="bla@some.com;john@home.com"
arr=(`echo $IN | tr ';' ' '`)

echo ${arr[0]}
echo ${arr[1]}

2

Используйте setвстроенный для загрузки $@массива:

IN="bla@some.com;john@home.com"
IFS=';'; set $IN; IFS=$' \t\n'

Тогда пусть вечеринка начнется:

echo $#
for a; do echo $a; done
ADDR1=$1 ADDR2=$2

Лучше использовать, set -- $INчтобы избежать некоторых проблем с $ IN, начинающимися с тире. Тем не менее, расширение без кавычек $INбудет расширять символы подстановки ( *?[).
Исаак

2

Две альтернативы bourne-ish, где ни один не требует массивов bash:

Случай 1 : Делайте это красиво и просто: используйте NewLine в качестве разделителя записей ... например.

IN="bla@some.com
john@home.com"

while read i; do
  # process "$i" ... eg.
    echo "[email:$i]"
done <<< "$IN"

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

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

Случай 2 : использование «;» в качестве разделителя записей ... например.

NL="
" IRS=";" ORS=";"

conv_IRS() {
  exec tr "$1" "$NL"
}

conv_ORS() {
  exec tr "$NL" "$1"
}

IN="bla@some.com;john@home.com"
IN="$(conv_IRS ";" <<< "$IN")"

while read i; do
  # process "$i" ... eg.
    echo -n "[email:$i]$ORS"
done <<< "$IN"

В обоих случаях под-список может быть составлен в цикле постоянным после завершения цикла. Это полезно при работе со списками в памяти, вместо хранения списков в файлах. {ps сохраняйте спокойствие и продолжайте B-)}


2

Помимо фантастических ответов, которые уже были предоставлены, если это просто вопрос распечатки данных, которые вы можете использовать awk:

awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN"

Это устанавливает разделитель полей на ;, чтобы он мог перебирать поля сfor цикла и печатать соответственно.

Тестовое задание

$ IN="bla@some.com;john@home.com"
$ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN"
> [bla@some.com]
> [john@home.com]

С другим входом:

$ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "a;b;c   d;e_;f"
> [a]
> [b]
> [c   d]
> [e_]
> [f]

2

В оболочке Android большинство предложенных методов просто не работают:

$ IFS=':' read -ra ADDR <<<"$PATH"                             
/system/bin/sh: can't create temporary file /sqlite_stmt_journals/mksh.EbNoR10629: No such file or directory

Что работает это:

$ for i in ${PATH//:/ }; do echo $i; done
/sbin
/vendor/bin
/system/sbin
/system/bin
/system/xbin

где //означает глобальную замену.


1
Сбой, если любая часть $ PATH содержит пробелы (или символы новой строки). Также расширяются подстановочные знаки (звездочка *, знак вопроса? И фигурные скобки […]).
Исаак

2
IN='bla@some.com;john@home.com;Charlie Brown <cbrown@acme.com;!"#$%&/()[]{}*? are no problem;simple is beautiful :-)'
set -f
oldifs="$IFS"
IFS=';'; arrayIN=($IN)
IFS="$oldifs"
for i in "${arrayIN[@]}"; do
echo "$i"
done
set +f

Вывод:

bla@some.com
john@home.com
Charlie Brown <cbrown@acme.com
!"#$%&/()[]{}*? are no problem
simple is beautiful :-)

Объяснение: Простое присваивание с использованием круглых скобок () преобразует список, разделенный точкой с запятой, в массив, если при этом у вас есть правильный IFS. Стандартный цикл FOR обрабатывает отдельные элементы в этом массиве как обычно. Обратите внимание, что список, заданный для переменной IN, должен быть «жестко» заключен в кавычки, то есть с одиночными тиками.

IFS должен быть сохранен и восстановлен, так как Bash не обрабатывает назначение так же, как команда. Альтернативный обходной путь - обернуть назначение внутри функции и вызвать эту функцию с измененным IFS. В этом случае отдельное сохранение / восстановление IFS не требуется. Спасибо за "Бизе" за указание на это.


!"#$%&/()[]{}*? are no problemну ... не совсем: []*?это глобус персонажи. Так как насчет создания этого каталога и файла: `mkdir '!" # $% &'; Touch '! "# $% & / () [] {} У вас есть хахахаха - нет проблем' и выполнение вашей команды? Простое может быть красивым, но когда оно сломано, оно сломано.
gniourf_gniourf

@gniourf_gniourf Строка хранится в переменной. Пожалуйста, смотрите оригинальный вопрос.
ajaaskel

1
@ajaaskel Вы не полностью поняли мой комментарий. Перейти в каталог царапанию и введите следующие команды: mkdir '!"#$%&'; touch '!"#$%&/()[]{} got you hahahaha - are no problem'. Я должен признать, что они будут создавать только каталог и файл со странными названиями. Затем запускать команды с точным INвы дали: IN='bla@some.com;john@home.com;Charlie Brown <cbrown@acme.com;!"#$%&/()[]{}*? are no problem;simple is beautiful :-)'. Вы увидите, что вы не получите ожидаемый результат. Потому что вы используете метод, подверженный раскрытию пути, чтобы разбить вашу строку.
gniourf_gniourf

Это должно продемонстрировать , что символы *, ?, [...]и даже, если extglobустановлен, то !(...), @(...), ?(...), +(...) являются проблемы с этим методом!
gniourf_gniourf

1
@gniourf_gniourf Спасибо за подробные комментарии по поводу глобализации. Я изменил код, чтобы отключить. Моя цель была, однако, просто показать, что довольно простое назначение может сделать работу разделения.
ajaaskel

1

Ладно, ребята!

Вот мой ответ!

DELIMITER_VAL='='

read -d '' F_ABOUT_DISTRO_R <<"EOF"
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04.4 LTS"
NAME="Ubuntu"
VERSION="14.04.4 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04.4 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
EOF

SPLIT_NOW=$(awk -F$DELIMITER_VAL '{for(i=1;i<=NF;i++){printf "%s\n", $i}}' <<<"${F_ABOUT_DISTRO_R}")
while read -r line; do
   SPLIT+=("$line")
done <<< "$SPLIT_NOW"
for i in "${SPLIT[@]}"; do
    echo "$i"
done

Почему этот подход "лучший" для меня?

По двум причинам:

  1. Вам не нужно избегать разделителя;
  2. У вас не будет проблем с пробелами . Значение будет правильно разделено в массиве!

[] 'S


К вашему сведению, /etc/os-releaseи /etc/lsb-releaseдолжны быть получены, а не проанализированы. Таким образом, ваш метод действительно неверен. Более того, вы не совсем отвечаете на вопрос о том, как разбить строку на разделитель.
gniourf_gniourf

0

Однострочник для разделения строки, разделенной ';' в массив это:

IN="bla@some.com;john@home.com"
ADDRS=( $(IFS=";" echo "$IN") )
echo ${ADDRS[0]}
echo ${ADDRS[1]}

Это только устанавливает IFS в подоболочке, поэтому вам не нужно беспокоиться о сохранении и восстановлении его значения.


-1 это не работает здесь (Ubuntu 12.04). он печатает только первый эхо со всеми значениями $ IN, в то время как второй пустой. Вы можете увидеть это, если поставить echo "0:" $ {ADDRS [0]} \ n echo "1:" $ {ADDRS [1]} - это вывод 0: bla@some.com;john@home.com\n 1:(\ n - новая строка)
Luca Borrione

1
пожалуйста, обратитесь к ответу nickjb для рабочей альтернативы этой идее stackoverflow.com/a/6583589/1032370
Luca Borrione

1
-1, 1. IFS не устанавливается в этом подоболочке (он передается в среду "echo", которая является встроенной, поэтому в любом случае ничего не происходит). 2. $INкотируется, поэтому не подлежит разделению IFS. 3. Подстановка процесса разделяется пробелами, но это может привести к повреждению исходных данных.
Score_Under

0

Возможно, не самое элегантное решение, но работает с *пробелами:

IN="bla@so me.com;*;john@home.com"
for i in `delims=${IN//[^;]}; seq 1 $((${#delims} + 1))`
do
   echo "> [`echo $IN | cut -d';' -f$i`]"
done

Выходы

> [bla@so me.com]
> [*]
> [john@home.com]

Другой пример (разделители в начале и в конце):

IN=";bla@so me.com;*;john@home.com;"
> []
> [bla@so me.com]
> [*]
> [john@home.com]
> []

В основном это удаляет каждый символ, кроме ;создания, delimsнапример. ;;;, Затем он выполняет forцикл от 1до, number-of-delimitersкак считается ${#delims}. Последний шаг - это безопасное $iиспользование cut.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.