Может ли IFS (Внутренний разделитель полей) функционировать как отдельный разделитель для нескольких последовательных символов-разделителей?

10

Синтаксический анализ массива с использованием IFS с не-белыми пробелами создает пустые элементы.
Даже использования tr -sдля сокращения нескольких разделителей до одного раздела недостаточно.
Пример может объяснить проблему более четко.
Есть ли способ достичь "нормальных" результатов с помощью настройки IFS (есть ли связанные настройки для изменения поведения IFS? .... т.е. действовать так же, как пробельные символы по умолчанию МФС.

var=" abc  def   ghi    "
echo "============== IFS=<default>"
arr=($var)
for x in ${!arr[*]} ; do
   echo "# arr[$x] \"${arr[x]}\""
done
#
sfi="$IFS" ; IFS=':'
set -f # Disable file name generation (globbing)
       # (This  data won't "glob", but unless globbing     
       #  is actually needed, turn if off, because   
       #  unusual/unexpected combinations of data can glob!
       #  and they can do it in the most obscure ways...  
       #  With IFS, "you're not in Kansas any more! :)  
var=":abc::def:::ghi::::"
echo "============== IFS=$IFS"
arr=($var)
for x in ${!arr[*]} ; do
   echo "# arr[$x] \"${arr[x]}\""
done
echo "============== IFS=$IFS and tr"
arr=($(echo -n "$var"|tr -s "$IFS"))
for x in ${!arr[*]} ; do
   echo "# arr[$x] \"${arr[x]}\""
done
set +f     # enable globbing 
IFS="$sfi" # re-instate original IFS val
echo "============== IFS=<default>"

Вот вывод


============== IFS=<default>
# arr[0] "abc"
# arr[1] "def"
# arr[2] "ghi"
============== IFS=:
# arr[0] ""
# arr[1] "abc"
# arr[2] ""
# arr[3] "def"
# arr[4] ""
# arr[5] ""
# arr[6] "ghi"
# arr[7] ""
# arr[8] ""
# arr[9] ""
============== IFS=: and tr
# arr[0] ""
# arr[1] "abc"
# arr[2] "def"
# arr[3] "ghi"
============== IFS=<default>
Peter.O
источник
Есть лучший (я думаю) ответ на тот же вопрос: stackoverflow.com/a/14789518/1765658
Ф. Хаури

Ответы:

3

Для удаления нескольких (не пробельных) последовательных символов-разделителей можно использовать два (строки / массива) расширения параметров. Хитрость заключается в том, чтобы установить IFSпеременную в пустую строку для расширения параметра массива.

Это описано в man bashразделе « Разделение слов» :

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

(
set -f
str=':abc::def:::ghi::::'
IFS=':'
arr=(${str})
IFS=""
arr=(${arr[@]})

echo ${!arr[*]}

for ((i=0; i < ${#arr[@]}; i++)); do 
   echo "${i}: '${arr[${i}]}'"
done
)
nazad
источник
Хороший! Простой и эффективный метод - без необходимости в цикле bash и без необходимости вызывать служебное приложение - кстати. Как вы упомянули «(не пробел)» , для ясности я бы отметил , что он прекрасно работает с любой комбинацией символов-разделителей, включая пробел.
Peter.O
В моих тестах настройка IFS=' '(то есть пробел) ведет себя так же. Я нахожу это менее запутанным, чем явный нулевой аргумент ("" или ") из IFS.
Миха Виденманн
Это своего рода ужасное решение, если ваши данные содержат встроенные пробелы. Таким образом, если ваши данные были «bc» вместо «abc», IFS = «» разделит «a» на отдельный элемент из «bc».
Деджей Клэйтон,
5

С bashmanpage:

Любой символ в IFS, который не является пробелом IFS, наряду с любыми соседними символами пробела IFS ограничивает поле. Последовательность пробельных символов IFS также рассматривается как разделитель.

Это означает, что пробелы IFS (пробел, табуляция и новая строка) не обрабатываются как другие разделители. Если вы хотите получить точно такое же поведение с альтернативным разделителем, вы можете выполнить замену разделителя с помощью trили sed:

var=":abc::def:::ghi::::"
arr=($(echo -n $var | sed 's/ /%#%#%#%#%/g;s/:/ /g'))
for x in ${!arr[*]} ; do
   el=$(echo -n $arr | sed 's/%#%#%#%#%/ /g')
   echo "# arr[$x] \"$el\""
done

Эта %#%#%#%#%вещь является магическим значением для замены возможных пробелов внутри полей, ожидается, что она будет «уникальной» (или очень нелепой). Если вы уверены, что в полях никогда не будет места, просто отбросьте эту часть).

jon_d
источник
@ FussyS ... Спасибо (см. Изменение в моем вопросе) ... Возможно, вы дали мне ответ на мой предполагаемый вопрос ... и этот ответ может быть (вероятно, есть) "Нет способа заставить IFS вести себя в так, как я хочу "... я намерен trпривести примеры, чтобы показать проблему ... Я хочу избежать системного вызова, поэтому я посмотрю на вариант bash, ${var##:}который я упомянул в своем комментарии к ответу Глена ... Я подожду некоторое время .. может быть, есть способ уговорить IFS, в противном случае первая часть вашего ответа будет после ....
Peter.O
Эта обработка IFSодинакова во всех оболочках в стиле Борна, она указана в POSIX .
Жиль "ТАК - прекрати быть злым"
4 с лишним года с тех пор, как я задал этот вопрос - я нашел ответ @ nazad (опубликованный более года назад) как самый простой способ манипулирования IFS для создания массива с любым числом и комбинацией IFSсимволов в качестве разделительной строки. На мой вопрос лучше всего ответили jon_d, но ответ @ nazad показывает изящный способ использования IFSбез циклов и служебных приложений.
Peter.O
2

Поскольку bash IFS не предоставляет внутренний способ обработки последовательных символов-разделителей как одного разделителя (для разделителей без пробелов), я собрал версию полностью bash (в отличие от использования внешнего вызова, например, tr, awk, sed). )

Он может обрабатывать мульти-символ IFS ..

Вот его результаты во время выполнения, наряду с аналогичными тестами для параметров trи awk, показанными на этой странице Q / A ... Тесты основаны на 10000 итерациях простого построения массива (без ввода-вывода) ...

pure bash     3.174s (28 char IFS)
call (awk) 0m32.210s  (1 char IFS) 
call (tr)  0m32.178s  (1 char IFS) 

Вот вывод

# dlm_str  = :.~!@#$%^&()_+-=`}{][ ";></,
# original = :abc:.. def:.~!@#$%^&()_+-=`}{][ ";></,'single*quote?'..123:
# unified  = :abc::::def::::::::::::::::::::::::::::'single*quote?'::123:
# max-w 2^ = ::::::::::::::::
# shrunk.. = :abc:def:'single*quote?':123:
# arr[0] "abc"
# arr[1] "def"
# arr[2] "'single*quote?'"
# arr[3] "123"

Вот сценарий

#!/bin/bash

# Note: This script modifies the source string. 
#       so work with a copy, if you need the original. 
# also: Use the name varG (Global) it's required by 'shrink_repeat_chars'
#
# NOTE: * asterisk      in IFS causes a regex(?) issue,     but  *  is ok in data. 
# NOTE: ? Question-mark in IFS causes a regex(?) issue,     but  ?  is ok in data. 
# NOTE: 0..9 digits     in IFS causes empty/wacky elements, but they're ok in data.
# NOTE: ' single quote  in IFS; don't know yet,             but  '  is ok in data.
# 
function shrink_repeat_chars () # A 'tr -s' analog
{
  # Shrink repeating occurrences of char
  #
  # $1: A string of delimiters which when consecutively repeated and are       
  #     considered as a shrinkable group. A example is: "   " whitespace delimiter.
  #
  # $varG  A global var which contains the string to be "shrunk".
  #
# echo "# dlm_str  = $1" 
# echo "# original = $varG" 
  dlms="$1"        # arg delimiter string
  dlm1=${dlms:0:1} # 1st delimiter char  
  dlmw=$dlm1       # work delimiter  
  # More than one delimiter char
  # ============================
  # When a delimiter contains more than one char.. ie (different byte` values),    
  # make all delimiter-chars in string $varG the same as the 1st delimiter char.
  ix=1;xx=${#dlms}; 
  while ((ix<xx)) ; do # Where more than one delim char, make all the same in varG  
    varG="${varG//${dlms:$ix:1}/$dlm1}"
    ix=$((ix+1))
  done
# echo "# unified  = $varG" 
  #
  # Binary shrink
  # =============
  # Find the longest required "power of 2' group needed for a binary shrink
  while [[ "$varG" =~ .*$dlmw$dlmw.* ]] ; do dlmw=$dlmw$dlmw; done # double its length
# echo "# max-w 2^ = $dlmw"
  #
  # Shrik groups of delims to a single char
  while [[ ! "$dlmw" == "$dlm1" ]] ; do
    varG=${varG//${dlmw}$dlm1/$dlm1}
    dlmw=${dlmw:$((${#dlmw}/2))}
  done
  varG=${varG//${dlmw}$dlm1/$dlm1}
# echo "# shrunk.. = $varG"
}

# Main
  varG=':abc:.. def:.~!@#$%^&()_+-=`}{][ ";></,'\''single*quote?'\''..123:' 
  sfi="$IFS"; IFS=':.~!@#$%^&()_+-=`}{][ ";></,' # save original IFS and set new multi-char IFS
  set -f                                         # disable globbing
  shrink_repeat_chars "$IFS" # The source string name must be $varG
  arr=(${varG:1})    # Strip leading dlim;  A single trailing dlim is ok (strangely
  for ix in ${!arr[*]} ; do  # Dump the array
     echo "# arr[$ix] \"${arr[ix]}\""
  done
  set +f     # re-enable globbing   
  IFS="$sfi" # re-instate the original IFS
  #
exit
Peter.O
источник
Отличная работа, интересная +1!
Ф. Хаури
1

Вы также можете сделать это с gawk, но это не красиво:

var=":abc::def:::ghi::::"
out=$( gawk -F ':+' '
  {
    # strip delimiters from the ends of the line
    sub("^"FS,"")
    sub(FS"$","")
    # then output in a bash-friendly format
    for (i=1;i<=NF;i++) printf("\"%s\" ", $i)
    print ""
  }
' <<< "$var" )
eval arr=($out)
for x in ${!arr[*]} ; do
  echo "# arr[$x] \"${arr[x]}\""
done

выходы

# arr[0] "abc"
# arr[1] "def"
# arr[2] "ghi"
Гленн Джекман
источник
Спасибо ... Я, кажется, не ясно в моем основном запросе (измененный вопрос) ... Это достаточно легко сделать, просто изменив мой $varна ${var##:}... Я действительно пытался настроить IFS сам ... Я хочу сделать это без внешнего вызова (у меня есть ощущение, что bash может сделать это более эффективно, чем любой внешний ... может, я буду продолжать в том же духе) ... ваш метод работает (+1) .... Насколько далеко так как изменение ввода идет, я бы предпочел попробовать его с помощью bash, а не awk или tr (это позволило бы избежать системного вызова), но я действительно
тусуюсь
@fred, как уже упоминалось, IFS только пропускает несколько последовательных разделителей для значения пробела по умолчанию. Иначе, последовательные разделители приводят к лишним пустым полям. Я ожидаю, что один или два внешних вызова чрезвычайно маловероятно повлияют на производительность любым реальным способом.
Гленн Джекман
@glen .. (Вы сказали, что ваш ответ не "симпатичный" .. Я думаю, что это так! :) Тем не менее, я собрал версию полностью bash (против внешнего вызова) и основанную на 10000 итерациях простого построения массива ( нет ввода / вывода) ... bash 1.276s... call (awk) 0m32.210s,,, call (tr) 0m32.178s... Сделайте это несколько раз, и вы можете подумать, что bash медленный! ... проще ли в этом случае? ... нет, если у вас уже есть фрагмент :) ... я опубликую его позже; должен идти сейчас.
Peter.O
Кстати, ваш сценарий gawk ... Я раньше практически не использовал awk, поэтому я рассмотрел его (и другие) подробно ... Я не могу выбрать почему, но упомяну проблема в любом случае .. Когда даны данные в кавычках, он теряет кавычки и разделяется на пробелы между кавычками ... и вылетает при нечетном количестве кавычек ... Вот тестовые данные:var="The \"X\" factor:::A single '\"' crashes:::\"One Two\""
Peter.O
-1

Ответ прост: сверните все разделители в один (первый).
Это требует цикла (который выполняется меньше, чем log(N)раз):

 var=':a bc::d ef:#$%_+$$%      ^%&*(*&*^
 $#,.::ghi::*::'                           # a long test string.
 d=':@!#$%^&*()_+,.'                       # delimiter set
 f=${d:0:1}                                # first delimiter
 v=${var//["$d"]/"$f"};                    # convert all delimiters to
 :                                         # the first of the delimiter set.
 tmp=$v                                    # temporal variable (v).
 while
     tmp=${tmp//["$f"]["$f"]/"$f"};        # collapse each two delimiters to one
     [[ "$tmp" != "$v" ]];                 # If there was a change
 do
     v=$tmp;                               # actualize the value of the string.
 done

Осталось только правильно разделить строку на один разделитель и вывести ее:

 readarray -td "$f" arr < <(printf '%s%s' "$v"'' "$f")
 printf '<%s>' "${arr[@]}" ; echo

Нет необходимости set -fни менять IFS.
Протестировано с пробелами, символами новой строки и глобусными символами. Все работают. Довольно медленный (как и следовало ожидать от цикла оболочки).
Но только для bash (bash 4.4+ из-за опции -dreadarray).


ш

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

 set -f; IFS=$f command eval set -- '$(echo "$var" | tr -s "$d" "[$f*]" )""'

И распечатать это:

 printf '<%s>' "$@" ; echo

Все еще медленно, но не намного.

Команда commandнедопустима в Борне.
В zsh commandвызывает только внешние команды и приводит к сбою eval, если commandиспользуется.
В ksh, даже при том command, что значение IFS изменяется в глобальной области видимости.
И commandделает разделение неудачным в оболочках, связанных с mksh (mksh, lksh, posh). Удаление команды commandзаставляет код работать на большем количестве оболочек. Но: удаление commandзаставит IFS сохранять свое значение в большинстве оболочек (eval - это специальная встроенная функция), за исключением bash (без режима posix) и zsh в режиме по умолчанию (без эмуляции). Эту концепцию нельзя заставить работать по умолчанию в zsh, с или без command.


Многосимвольный IFS

Да, IFS может быть многосимвольным, но каждый символ будет генерировать один аргумент:

 set -f; IFS="$d" command eval set -- '$(echo "$var" )""'
 printf '<%s>' "$@" ; echo

Будет выводить:

 <><a bc><><d ef><><><><><><><><><      ><><><><><><><><><
 ><><><><><><ghi><><><><><>

С bash вы можете опустить commandслово, если не в эмуляции sh / POSIX. Команда завершится ошибкой в ​​ksh93 (IFS сохраняет измененное значение). В zsh команда commandзаставляет zsh пытаться найти evalвнешнюю команду (которую она не находит) и завершается неудачно.

То, что происходит, - то, что единственные символы IFS, которые автоматически свернуты в один разделитель, являются пробелом IFS.
Один пробел в IFS свернет все последовательные пробелы в один. Одна вкладка свернет все вкладки. Один пробел и одна вкладка сворачивают серии пробелов и / или табуляций в один разделитель. Повторите идею с новой строкой.

Чтобы свернуть несколько разделителей, требуется некоторое жонглирование.
Предполагая, что ASCII 3 (0x03) не используется во входных данных var:

 var=${var// /$'\3'}                       # protect spaces
 var=${var//["$d"]/ }                      # convert all delimiters to spaces
 set -f;                                   # avoid expanding globs.
 IFS=" " command eval set -- '""$var""'    # split on spaces.
 set -- "${@//$'\3'/ }"                    # convert spaces back.

Большинство комментариев о ksh, zsh и bash (about commandи IFS) все еще применимы здесь.

Значение $'\0'будет менее вероятным при вводе текста, но переменные bash не могут содержать NUL ( 0x00).

В sh нет внутренних команд для выполнения одинаковых строковых операций, поэтому tr - единственное решение для sh-скриптов.

Исаак
источник
Да, я написал, что для оболочки ОП попросил: Bash. В этой оболочке IFS не сохраняется. И да, не переносимо, к зш, например. @ StéphaneChazelas
Исаак
В случае bash и zsh они ведут себя так, как POSIX указывает при вызове как sh
Стефан
@ StéphaneChazelas Добавлено (много) заметок об ограничениях каждой оболочки.
Исаак
@ StéphaneChazelas Почему понизить?
Исаак
Не знаю, не я. Кстати, я думаю, что здесь есть специальные вопросы и ответы о command evalIIRC от Жиля
Стефан