Разделить строку с помощью IFS


8

Я написал пример сценария для разделения строки, но он не работает должным образом

#!/bin/bash
IN="One-XX-X-17.0.0"
IFS='-' read -r -a ADDR <<< "$IN"
for i in "${ADDR[@]}"; do
 echo "Element:$i"
done
#split 17.0.0 into NUM
IFS='.' read -a array <<<${ADDR[3]};
for element in "${array[@]}"
do
    echo "Num:$element"
done

вывод

One
XX
X
17.0.0
17 0 0

но я ожидал, что результат будет:

      One
      XX
      X
      17.0.0
      17
      0
      0

Кстати, если один из ответов ниже решил вашу проблему, пожалуйста, найдите время и примите его , нажав на флажок слева. Это пометит вопрос как ответивший и выразит благодарность на сайтах Stack Exchange.
Тердон

Ответы:


2

Исправьте (см. Также ответ С. Чазеласа для фона), с разумным выводом:

#!/bin/bash
IN="One-XX-X-17.0.0"
IFS='-' read -r -a ADDR <<< "$IN"
for i in "${ADDR[@]}"; do
    if [ "$i" = "${i//.}" ] ; then 
        echo "Element:$i" 
        continue
    fi
    # split 17.0.0 into NUM
    IFS='.' read -a array <<< "$i"
    for element in "${array[@]}" ; do
        echo "Num:$element"
    done
done

Вывод:

Element:One
Element:XX
Element:X
Num:17
Num:0
Num:0

Ноты:

  • Лучше поставить условный 2 - й цикл в в 1 - м цикле.

  • bashpattern substitution ( "${i//.}") проверяет .наличие элемента. ( caseУтверждение может быть проще, хотя и менее похоже на код OP .)

  • readING $array, вводя <<< "${ADDR[3]}"меньше общего , чем <<< "$i". Это позволяет избежать необходимости знать, какой элемент имеет .s.

  • Код предполагает, что печать « Элемент: 17.0.0 » является непреднамеренной. Если Такое поведение является предназначено заменить основной цикл с:

    for i in "${ADDR[@]}"; do
       echo "Element:$i" 
       if [ "$i" != "${i//.}" ] ; then 
       # split 17.0.0 into NUM
           IFS='.' read -a array <<< "$i"
           for element in "${array[@]}" ; do
               echo "Num:$element"
           done
       fi
    done

1
case $i in (*.*) ...был бы более канонический способ проверки, который $iсодержит .(и также переносим sh). Если вы любите кшизмы, см. Также:[[ $i = *.* ]]
Стефан

@ StéphaneChazelas, уже упоминалось caseв примечаниях в конце, но мы согласны. (Так как OP использует оба <<<и массивы , это не большой shвопрос.)
agc

10

В старых версиях bashвам приходилось заключать в кавычки переменные после <<<. Это было исправлено в 4.4. В более старых версиях переменная разделялась на IFS, и результирующие слова объединялись в пространстве перед сохранением во временном файле, который составляет это <<<перенаправление.

В 4.2 и ранее, при перенаправлении встроенных функций, таких как readили command, такое разбиение даже получило бы IFS для этой встроенной функции (4.3 исправило это):

$ bash-4.2 -c 'a=a.b.c.d; IFS=. read x <<< $a; echo  "$x"'
a b c d
$ bash-4.2 -c 'a=a.b.c.d; IFS=. cat <<< $a'
a.b.c.d
$ bash-4.2 -c 'a=a.b.c.d; IFS=. command cat <<< $a'
a b c d

Это исправлено в 4.3:

$ bash-4.3 -c 'a=a.b.c.d; IFS=. read x <<< $a; echo  "$x"'
a.b.c.d

Но там $aвсе еще есть разделение слов:

$ bash-4.3 -c 'a=a.b.c.d; IFS=.; read x <<< $a; echo  "$x"'
a b c d

В 4.4:

$ bash-4.4 -c 'a=a.b.c.d; IFS=.; read x <<< $a; echo  "$x"'
a.b.c.d

Для переносимости в более старые версии, укажите вашу переменную (или используйте, zshоткуда она <<<взялась, и у которой нет этой проблемы)

$ bash-any-version -c 'a=a.b.c.d; IFS=.; read x <<< "$a"; echo "$x"'
a.b.c.d

Обратите внимание, что такой подход для разделения строки работает только для строк, которые не содержат символов новой строки. Также не отметить , что a..b.c.должно быть разделено на "a", "", "b", "c"(не пустой последний элемент).

Чтобы разделить произвольные строки, вы можете использовать вместо этого оператор split + glob (который сделает его стандартным и позволит избежать хранения содержимого переменной во временном файле, как это <<<делается):

var='a.new
line..b.c.'
set -o noglob # disable glob
IFS=.
set -- $var'' # split+glob
for i do
  printf 'item: <%s>\n' "$i"
done

или:

array=($var'') # in shells with array support

''Является сохранение завершающую пустой элемент , если таковые имеются. Это также разделило бы пустое $varна один пустой элемент.

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

  • zsh:

    array=(${(s:.:)var} # removes empty elements
    array=("${(@s:.:)var}") # preserves empty elements
  • rc:

    array = ``(.){printf %s $var} # removes empty elements
  • fish

    set array (string split . -- $var) # not for multiline $var

1

С awk это обойдется вам в одну строку:

IN="One-XX-X-17.0.0"

awk -F'[-.]' '{ for(i=1;i<=NF;i++) printf "%s : %s\n",($i~/^[0-9]+$/?"Num":"Element"),$i }' <<<"$IN"
  • -F'[-.]'- разделитель полей, основанный на нескольких символах, в нашем случае -и.

Выход:

Element : One
Element : XX
Element : X
Num : 17
Num : 0
Num : 0

То же самое можно сделать сIFS=-. read -r a array <<< "$IN"
Stéphane Chazelas

@ StéphaneChazelas, это другое. Вы показываете только шаг с преобразованием строки в массив. Но моя одна строка посвящена тому, чтобы охватить все: разделение на поля, обработку и вывод. Я не
поспорю

0

Вот мой путь:

OIFS=$IFS
IFS='-'
IN="One-XX-X-17.0.0"
ADDR=($IN)
for i in "${ADDR[@]}"; do
 echo "Element:$i"
done
IFS='.'
array=(${ADDR[3]})
for element in "${array[@]}"
do
  echo "Num:$element"
done

результат, как и ожидалось:

Num:17
Num:0
Num:0

Это $INвызывает оператор split + glob. Здесь вам не нужна глобальная часть (примерьте, IN=*-*-/*-17.0.0например), поэтому вы захотите сделать это set -o noglobперед ее вызовом. Смотрите мой ответ для деталей.
Стефан

1
В общем, старайтесь избегать «сохранения» IFSи установки его глобально. Вы действительно хотите изменить только значение, IFSкогда $INраскрывается, и вы также не хотите, чтобы расширение расширения выполнялось для расширения. Кроме того, OIFS=$IFSне делает различий между случаями, когда IFSбыла задана пустая строка, и когда она IFSбыла полностью не установлена.
chepner
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.