it-swarm.com.ru

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

Я храню эту строку в переменной:

IN="[email protected];[email protected]"

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

ADDR1="[email protected]"
ADDR2="[email protected]"

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


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

#!/usr/bin/env bash

IN="[email protected];[email protected]"

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

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

Результат:

> [[email protected]]
> [[email protected]]

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

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

IN="[email protected];[email protected]"

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

IFS=$OIFS

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

mails2=($IN)

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

1751
stefanB

Вы можете установить переменную внутренний разделитель полей (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"
1082
Johannes Schaub - litb

Взято из разделенный массив сценариев Bash Shell:

IN="[email protected];[email protected]"
arrIN=(${IN//;/ })

Объяснение:

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

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

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

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

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

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

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

222
Chris Lutz

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

На этот вопрос SO уже есть много разных способов сделать это в bash . Но у bash есть много специальных функций, так называемых bashism, которые работают хорошо, но это не работать в любой другой Shell .

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

На моем Debian GNU/Linux есть стандартная оболочка, называемая тире , но я знаю много люди, которые любят использовать ksh .

Наконец, в очень маленькой ситуации есть специальный инструмент под названием busybox со своим собственным интерпретатором Shell ( ash ).

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

Пример строки в вопросе SO:

IN="[email protected];[email protected]"

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

 IN="[email protected];[email protected];Full Name <[email protected]>"

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

В pure bash мы можем использовать массивы и IFS:

var="[email protected];[email protected];Full Name <[email protected]>"
oIFS="$IFS"
IFS=";"
declare -a fields=($var)
IFS="$oIFS"
unset oIFS
IFS=\; read -a fields <<<"$IN"

Используя этот синтаксис в недавнем bash, не меняйте $IFS для текущего сеанса, а только для текущей команды:

set | grep ^IFS=
IFS=$' \t\n'

Теперь строка var разделена и сохранена в массив (с именем fields):

set | grep ^fields=\\\|^var=
fields=([0]="[email protected]" [1]="[email protected]" [2]="Full Name <[email protected]>")
var='[email protected];[email protected];Full Name <[email protected]>'

Мы можем запросить переменное содержимое с помощью declare -p:

declare -p IN fields
declare -- IN="[email protected];[email protected];Full Name <[email protected]>"
declare -a fields=([0]="[email protected]" [1]="[email protected]" [2]="Full Name <[email protected]>")

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

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

for x in "${fields[@]}";do
    echo "> [$x]"
    done
> [[email protected]]
> [[email protected]]
> [Full Name <[email protected]>]

или отбросить каждое поле после обработки (мне нравится этот подход shifting ):

while [ "$fields" ] ;do
    echo "> [$fields]"
    fields=("${fields[@]:1}")
    done
> [[email protected]]
> [[email protected]]
> [Full Name <[email protected]>]

или даже для простой распечатки (более короткий синтаксис):

printf "> [%s]\n" "${fields[@]}"
> [[email protected]]
> [[email protected]]
> [Full Name <[email protected]>]

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

Вы можете играть с mapfile:

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

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

Если вам не нужны пустые поля, вы можете:

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

Но вы можете использовать поля через функцию:

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 '[email protected]', done.
Seq:      1: Sending mail to '[email protected]', done.
Seq:      2: Sending mail to 'Full Name <[email protected]>', done.

Или же Удалить новую строку, добавленную синтаксисом bash <<<, в функцию:

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

Будет отображать тот же результат:

Seq:      0: Sending mail to '[email protected]', done.
Seq:      1: Sending mail to '[email protected]', done.
Seq:      2: Sending mail to 'Full Name <[email protected]>', done.

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

Но если вы хотите написать что-то пригодное для использования под многими оболочками, вы должны не использовать bashisms .

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

${var#*SubStr}  # will drop begin of string up to first occur of `SubStr`
${var##*SubStr} # will drop begin of string up to last occur of `SubStr`
${var%SubStr*}  # will drop part of string from last occur of `SubStr` to the end
${var%%SubStr*} # will drop part of string from first occur of `SubStr` to the end

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

Как указано Score_Under :

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

## и %% удаляют самое длинное из возможных.

где # и ## означают слева (начало) строки, и

% и %% meand справа (конец) строки.

Этот небольшой пример скрипта хорошо работает под bash , тире , ksh , busybox и был протестирован в bash Mac-OS тоже:

var="[email protected];[email protected];Full Name <[email protected]>"
while [ "$var" ] ;do
    iter=${var%%;*}
    echo "> [$iter]"
    [ "$var" = "$iter" ] && \
        var='' || \
        var="${var#*;}"
  done
> [[email protected]]
> [[email protected]]
> [Full Name <[email protected]>]

Повеселись!

161
F. Hauri

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

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

Пример:

$ echo "[email protected];[email protected]" | cut -d ";" -f 1
[email protected]
$ echo "[email protected];[email protected]" | cut -d ";" -f 2
[email protected]

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

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

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

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

128
DougW

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

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

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

IN="[email protected];[email protected]" 
set -- "$IN" 
IFS=";"; declare -a Array=($*) 
echo "${Array[@]}" 
echo "${Array[0]}" 
echo "${Array[1]}" 

Источник

84
errator
62
lothar

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

IN="[email protected];[email protected]"
echo ADD1=`echo $IN | cut -d \; -f 1`
echo ADD2=`echo $IN | cut -d \; -f 2`

Будьте осторожны, это решение не всегда правильно. Если вы передадите только "[email protected]", он назначит его как ADD1, так и ADD2.

62
Ashok

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

echo "[email protected];[email protected]" | awk -F';' '{print $1,$2}'

дам

[email protected] [email protected]

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

38
Tony

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

IN="[email protected];[email protected]"
read ADDR1 ADDR2 <<<$(IFS=";"; echo $IN)
30
nickjb

В 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")'

Хитрость для этого заключается в том, чтобы использовать параметр -dread (разделитель) с пустым разделителем, чтобы 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")
26
gniourf_gniourf

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

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

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

in="[email protected];[email protected];[email protected];[email protected]"
IFS=';' list=($in)
for item in "${list[@]}"; do echo $item; done

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

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

19
kenorb

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

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

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

ты получишь:

b = foo
c = bar
16
Emilien Brigand

Следующая функция 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 "[email protected]"
9
Halle Knast

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

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 {}
7
Victor Choy

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

echo "[email protected];[email protected]"|awk -F';' '{printf "%s\n%s\n", $1, $2}'

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

echo "[email protected];[email protected]"|awk -F';' '{print $1,$2}' OFS="\n"
6
shuaihanhungry

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

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

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

IN="[email protected];[email protected]"
declare -a a="(${IN/;/ })";

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

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

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

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

4
eukras
IN="[email protected];[email protected]"
IFS=';'
read -a IN_arr <<< "${IN}"
for entry in "${IN_arr[@]}"
do
    echo $entry
done

Результат

[email protected]
[email protected]

Система: Ubuntu 12.04.1

3
rashok

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

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

IN="[email protected]
[email protected]"

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="[email protected];[email protected]"
IN="$(conv_IRS ";" <<< "$IN")"

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

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

2
NevilleDNZ

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

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

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

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

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

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

$ 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
fedorqui

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

IN="[email protected];[email protected]"
arr=(`echo $IN | tr ';' ' '`)

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

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

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

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

1
Eduardo Lucio
IN='[email protected];[email protected];Charlie Brown <[email protected];!"#$%&/()[]{}*? 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

Результат:

[email protected]
[email protected]
Charlie Brown <[email protected]
!"#$%&/()[]{}*? are no problem
simple is beautiful :-)

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

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

1
ajaaskel

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

$ 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
18446744073709551615

Используйте встроенную переменную set для загрузки массива [email protected]:

IN="[email protected];[email protected]"
IFS=';'; set $IN; IFS=$' \t\n'

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

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

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

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

Результаты

> [[email protected] me.com]
> [*]
> [[email protected]]

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

IN=";[email protected] me.com;*;[email protected];"
> []
> [[email protected] me.com]
> [*]
> [[email protected]]
> []

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

0
Petr Újezdský

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

IN="[email protected];[email protected]"
ADDRS=( $(IFS=";" echo "$IN") )
echo ${ADDRS[0]}
echo ${ADDRS[1]}

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

0
Michael Hale

Это будет даже обрабатывать пробелы:

IFS=';' read ADDR1 ADDR2 <<< $(echo ${IN})
0
Mat Bess