Як розділити рядок на роздільнику в Bash?


2040

Цей рядок у мене зберігається в змінній:

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 повернеться до того, що було спочатку.
сумнівним

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

4
for x in $(IFS=';';echo $IN); do echo "> [$x]"; done
користувач2037659

2
Щоб зберегти його як масив, мені довелося розмістити ще один набір дужок і змінити на \nпросто пробіл. Отже, фінальна лінія є mails=($(echo $IN | tr ";" " ")). Тож тепер я можу перевірити елементи mails, використовуючи позначення масиву mails[index]або просто
ітерацію

Відповіді:


1232

Ви можете встановити змінну внутрішнього роздільника поля (IFS), а потім дозволити її розбору в масив. Коли це відбувається в команді, тоді присвоєння IFSлише відбувається оточенню однієї команди (до 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
тепер після застосування виправлення, лише протягом тривалості команди read :)
Йоханнес Шауб - litb

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

55
@LucaBorrione Встановлення IFSв тому ж рядку, що readі без крапки з комою чи іншим роздільником, на відміну від окремої команди, поширює його на цю команду - тому вона завжди "відновлюється"; вам не потрібно нічого робити вручну.
Чарльз Даффі

5
@imagineerThis Існує помилка, пов’язана з єрестами та локальними змінами в IFS, яку потрібно $INнавести. Помилка зафіксована в bash4.3.
чепнер

972

Взятий з розділеного масиву сценарію оболонки 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 зворотного зв’язку і не визначив :)
Йоханнес Шауб - ліб

33
-1, ви, очевидно, не знаєте розбиття слів, оскільки це вводить дві помилки у ваш код. один - коли ви не цитуєте $ IN, а інший - коли ви робите вигляд, що новий рядок є єдиним роздільником, який використовується у розбиванні слів. Ви повторюєте кожне слово в IN, а не кожен рядок, і ВИЗНАЧЕНО не кожен елемент, розділений крапкою з комою, хоча, здається, має побічний ефект, схожий на те, що він працює.
lhunath

3
Ви можете змінити його, щоб перегукуватися з "$ IN" | tr ';' '\ n' | під час читання -r ДОБАВИТИ; виконувати # обробку "$ ADDY"; зроблено, щоб йому пощастило, я думаю :) Зауважте, що це буде виделкою, і ви не можете змінювати зовнішні змінні з циклу (саме тому я використав синтаксис <<< "$ IN")
Йоганнес Шауб - ліб

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

202

Сумісна відповідь

Існує маса різних способів зробити це .

Однак важливо спершу зазначити, що bashє багато особливих особливостей (так званих башизмів ), які не працюватимуть ні в якому іншому.

Зокрема, масиви , асоціативні масиви та заміна шаблонів , які використовуються в рішеннях на цій посаді, а також інші в потоці, є башизмами і можуть не працювати під іншими оболонками , якими користуються багато людей.

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

Однак нам не потрібно використовувати масиви для переходу на "елементи" рядка. Існує синтаксис, який використовується в багатьох оболонках для видалення підрядків рядка з першого або останнього появи шаблону. Зауважте, що *це підстановка, яка означає нуль або більше символів:

(Відсутність такого підходу в будь-якому досі розміщеному рішенні є основною причиною написання цієї відповіді;)

${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
Kudos для використання cut, це правильний інструмент для роботи! Набагато очищені, ніж будь-який із цих злому.
MisterMiyagi

4
Цей підхід спрацює, лише якщо ви заздалегідь знаєте кількість елементів; вам потрібно буде запрограмувати ще трохи логіки навколо цього. Він також запускає зовнішній інструмент для кожного елемента.
uli42

Надзвичайно я шукав спроби уникнути порожнього рядка в csv. Тепер я можу вказати і точне значення "стовпця". Робота з IFS, що вже використовується в циклі. Краще, ніж очікувалося для моєї ситуації.
Луї Лудог Троттьє

Дуже корисно для протягання ідентифікаторів і ИДП теж є
Мілош Груїчевої

Цю відповідь варто прокрутити на половину сторінки :)
Gucu112

124

Це працювало для мене:

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

1
Хоча він працює лише з роздільником символів, саме це шукало ОП (записи, розділені крапкою з комою).
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 ... але я б не назвав змінну "масив" ... pet peev я думаю. Гарне рішення.
Ізмір Рамірес

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

+1 Лише бічна примітка: чи не слід рекомендувати зберігати старий IFS і потім його відновлювати? (як показав stefanB у своїй редакції3) люди, які приземляються сюди (іноді просто копіюючи та вставляючи рішення), можуть не замислюватися над цим
Лука Борріоне

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
Або навіть простіше: відлуння "bla@some.com; john@home.com" | awk 'ПОЧАТИ {RS = ";"} {print}'
Джаро

@Jaro Це чудово працювало для мене, коли у мене був рядок із комами та мені потрібно було переформатувати його на рядки. Дякую.
Акварель

Це працювало в цьому сценарії -> "echo" $ SPLIT_0 "| awk -F 'inode =' '{print $ 1}'"! У мене виникли проблеми при спробі використовувати atrings ("inode =") замість символів (";"). $ 1, $ 2, $ 3, $ 4 встановлюються як позиції в масиві! Якщо є спосіб встановити масив ... краще! Дякую!
Едуардо Лусіо

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

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 (елемент для кожного рядка розділено двокрапкою)
Luca Borrione

3
@Luca Ні сценарій sed не створює рівно двох рядків. Що створює кілька записів для вас, це коли ви поміщаєте його в масив bash (який за замовчуванням розпадається на пробіл)
lothar

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

1
@Luca Добре. Однак призначення масиву не було в початковому запитанні, коли я писав цю відповідь.
lothar

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 = СПИСОК виберіть лише ці поля; також надрукуйте будь-який рядок, що не містить символу роздільника, якщо опція -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)нижній частині; саме тому деякі читачі (включаючи мене) спочатку думають, що це не вийде. Я припускав, що ADDR1 придушив всі $ IN. Але nickjb правильний; це працює. Причина полягає в тому, що echo $INкоманда аналізує свої аргументи, використовуючи поточне значення $ IFS, але потім повторює їх для stdout, використовуючи роздільник пробілу, незалежно від налаштування $ IFS. Таким чином, чистий ефект як би хтось викликав read ADDR1 ADDR2 <<< "bla@some.com john@home.com"(зауважте, що введення розділено пробілом не; -розділене).
сумнівним

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(ака readarray) підтримує -dможливість вказати роздільник. Звідси ще один канонічний спосіб:

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

5
Я знайшов це як рідкісне рішення у цьому списку, яке коректно працює з \nпробілами та *одночасно. Також ніяких петель; Змінна масиву є доступною в оболонці після виконання (всупереч найбільш відповідальній відповіді). Зауважте, in=$'...'це не працює з подвійними лапками. Я думаю, що для цього потрібно більше результатів.
John_West

28

Як щодо цього одного вкладиша, якщо ви не використовуєте масиви:

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

Розглянемо, read -r ...щоб переконатися, що, наприклад, два символи "\ t" у вводі закінчуються такими ж двома символами у змінних (замість однієї таблиці вкладки).
сумнівним

-1 Тут не працює (ubuntu 12.04). Додавання echo "ADDR1 $ADDR1"\n echo "ADDR2 $ADDR2"до вашого фрагмента виведе ADDR1 bla@some.com john@home.com\nADDR2(\ n це новий рядок)
Лука Borrione

Ймовірно, це пов'язано з помилкою, що включає IFSі тут рядки, які були виправлені в bash4.3. Цитування $INповинно це виправити. (Теоретично $INне підлягає розщепленню чи поглибленню слів після його розширення. Це означає, що лапки повинні бути непотрібними. Навіть у 4.3, однак, залишається хоча б одна помилка - повідомляється та планується виправити - тому цитування залишається хорошим ідея.)
чепнер

Це порушується, якщо $ in містить нові рядки, навіть якщо вказано $ IN. І додає зворотний новий рядок.
Ісаак

Проблема з цим та багатьма іншими рішеннями також полягає в тому, що він передбачає, що В $ IN - ТОЧНО ДВА ЕЛЕМЕНТИ АБО - що Ви готові другий та наступні елементи розбити разом у ADDR2. Я розумію, що це відповідає запиту, але це бомба часу.
Стівен Легко

21

Без встановлення 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 та -Aв zsh.

При бажанні функцію можна ввести в сценарій наступним чином:

#!/usr/bin/env bash

split() {
    # ...
}

split "$@"

Здається, не працює з роздільниками довше 1 символу: split = $ (розділити файл "$ content" ": //")
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. Якщо ви використовуєте яблучний мак, як я. Ви можете встановити 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

Тут є кілька цікавих відповідей (помилковий есп.), Але для того, щоб щось аналогічне розділенню іншими мовами, - що я вважав оригінальним питанням, - я вирішив це:

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містить більше двох електронних адрес. Зверніться до цієї ж ідеї (але виправлено) у відповіді паліндром
олібре

Краще використовувати ${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 всередині , а лише переходити до іншого РС, коли генерує кінцевий результат зовнішньо .

Випадок 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 Рядок зберігається у змінній. Будь ласка, дивіться оригінальне запитання.
Аджаскель

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 Дякую за детальні коментарі щодо глобалізації. Я відкоригував код, щоб вимкнути глобус. Моя думка, однак, полягає лише в тому, щоб показати, що досить просте завдання може виконати поділ.
Аджаскель

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. У вас не виникне проблем із порожніми пробілами . Значення буде належним чином відокремлено в масиві!

[]


FYI, /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 IN, а друге порожнє. ви можете побачити це, якщо поставити echo "0:" $ {ADDRS [0]} \ n echo "1:" $ {ADDRS [1]} вихід 0: bla@some.com;john@home.com\n 1:(\ n новий рядок)
Лука Borrione

1
зверніться до відповіді nickjb на робочу альтернативу цій ідеї stackoverflow.com/a/6583589/1032370
Лука Borrione

1
-1, 1. IFS не встановлюється в цій підпакеті (передається в середовище "відлуння", яке є вбудованим, тому все одно нічого не відбувається). 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.