Чи може IFS (Internal Field Separator) функціонувати як єдиний роздільник для декількох послідовних знаків розмежувача?


10

Розбір масиву за допомогою IFS з небілими значеннями простору створює порожні елементи.
Навіть використання tr -sдля зменшення декількох розрядів на один делім недостатньо.
Приклад може пояснити проблему більш чітко.
Чи є спосіб досягти "нормальних" результатів за допомогою налаштування IFS (чи є пов'язане налаштування для зміни поведінки 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
F. Hauri

Відповіді:


3

Для видалення декількох (непробільних) послідовних розділових знаків можна використовувати два (рядок / масив) параметрів розширення. Трюк полягає у встановленні IFSзмінної у порожній рядок для розширення параметра масиву.

Це задокументовано у man bashрозділі Word Splitting :

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

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

У моїх тестах налаштування IFS=' '(тобто пробіл) поводиться так само. Я вважаю це менш заплутаним, ніж явний нульовий аргумент ("" або "") IFS.
Micha Wiedenmann

Це жахливе рішення, якщо ваші дані містять вбудований пробіл. Це, якби ваші дані були "a bc" замість "abc", IFS = "" розділить "a" на окремий елемент від "bc".
Dejay Clayton

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

%#%#%#%#%Річ магічна значення , щоб замінити можливі прогалини в полях, як очікується, буде «унікальний» (або дуже unlinkely). Якщо ви впевнені, що в полях ніколи не буде місця, просто опустіть цю частину).


@FussyS ... Дякую (див. Модифікацію в моєму запитанні) ... Можливо, ти дав мені відповідь на моє призначене питання .. і ця відповідь може бути (ймовірно, є) "Немає способу змусити IFS вести себе в Таким чином, я хочу "... Я націлюсь на trприклади, щоб показати проблему ... Я хочу уникнути системного виклику, тому я ${var##:}роздивлюсь варіант bash, поза тим, про який я згадував у своєму коментарі до передбіжника Глена .... Я зачекаю деякий час .. можливо, є спосіб придушити IFS, інакше перша частина вашої відповіді була після ....
Пітер.O

Ця обробка IFSоднакова у всіх оболонках у стилі Борна, вона вказана в POSIX .
Жил "ТАК - перестань бути злим"

4 роки з того часу, як я задав це запитання - я знайшов відповідь @ назад (опубліковану понад рік тому) як найпростіший спосіб перемикати IFS, щоб створити масив з будь-яким числом і комбінацією IFSсимволів як розділовий рядок. Найкраще відповів на моє запитання jon_d, але відповідь @ назад показує чудовий спосіб використання IFSбез циклів і ніяких утиліт.
Пітер.О

2

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

Він може обробляти багатоканальні IFS ..

Ось його час виконання; ts, а також аналогічні тести для 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 .. Я хочу робити це без зовнішнього дзвінка (у мене є відчуття, що баш може зробити це ефективніше, ніж будь-який зовнішній може .. тому я продовжуватиму цей шлях) ... ваш метод працює (+1) .... Наскільки що стосується зміни вхідних даних, я вважаю за краще спробувати це з bash, а не awk або tr (це дозволить уникнути системного виклику), але я дійсно
зависаю

@fred, як уже згадувалося, IFS лише збільшує кілька послідовних деліметрів для значення пробілу за замовчуванням. В іншому випадку послідовні роздільники призводять до сторонніх порожніх полів. Я очікую, що один-два зовнішніх дзвінки навряд чи вплинуть на продуктивність реальним чином.
glenn jackman

@glen .. (Ви сказали, що ваша відповідь не "гарна". Я думаю, що це! :) Однак я зібрав всю версію bash (проти зовнішнього виклику) і базуючись на 10000 ітераціях просто створення масиву ( немає вводу-виводу ... bash 1.276s... call (awk) 0m32.210s,,, call (tr) 0m32.178s... Зробіть це кілька разів, і ви можете подумати, що баш повільний! ... Чи легше у цьому випадку awk? ... ні, якщо ви вже отримали фрагмент :) ... я опублікую його пізніше; треба йти зараз.
Пітер.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+ через можливість -dчитання масиву).


ш

Версія оболонки не може використовувати масив, єдиний доступний масив - це позиційні параметри.
Використання tr -s- це лише один рядок (IFS не змінюється в сценарії):

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

І роздрукуйте:

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

Ще повільно, але не набагато більше.

Команда commandв Борні недійсна.
У zsh commandвикликає лише зовнішні команди та робить eval fail, якщо commandвикористовується.
У кш, навіть з 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 (про commandта IFS) все ще стосуються тут.

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

У SH немає внутрішніх команд для виконання тих же строкових операцій, тому tr - єдине рішення для скриптів sh.


Так, я написав, що за оболонку ОП просила: Баш. У цій оболонці IFS не зберігається. І так, не портативний, наприклад, zsh. @ StéphaneChazelas
Ісаак

Що стосується bash та zsh, вони поводяться так, як вказує POSIX, коли викликаються як sh
Stéphane Chazelas

@ StéphaneChazelas Додано (багато) приміток про обмеження кожної оболонки.
Ісаак

@ StéphaneChazelas Чому потік?
Ісаак

Не знаю, чи не я. До речі, я думаю, що тут присвячений питання щодо command evalIIRC від Gilles
Stéphane Chazelas
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.