Может ли 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>

Есть лучший (я думаю) ответ на тот же вопрос: 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
)

Хороший! Простой и эффективный метод - без необходимости в цикле 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

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


@ 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

Отличная работа, интересная +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 от Жиля
— Стефан
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.