Czy IFS (Internal Field Separator) może działać jako pojedynczy separator dla wielu kolejnych znaków separatora?

10

Analiza składni tablicy za pomocą IFS z wartościami spacji niebiałych tworzy puste elementy.
Nawet użycie tr -sdo zmniejszenia wielu delimów do jednego delim nie wystarczy.
Przykład może jaśniej wyjaśnić ten problem.
Czy istnieje sposób na osiągnięcie „normalnych” wyników poprzez udoskonalenie IFS (czy istnieje powiązane ustawienie zmieniające zachowanie IFS? .... tzn. Działanie tak samo jak domyślna biała spacja 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>"

Oto wynik


============== 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
źródło
Jest lepsza (myślę) odpowiedź na to samo pytanie: stackoverflow.com/a/14789518/1765658
F. Hauri

Odpowiedzi:

3

Aby usunąć wiele znaków spacji (kolejnych spacji), można użyć dwóch rozszerzeń parametrów (ciąg / tablica). Sztuczka polega na ustawieniu IFSzmiennej na pusty ciąg znaków dla rozszerzenia parametru tablicy.

Jest to udokumentowane w man bashrozdziale Podział słów :

Niecytowane niejawne argumenty zerowe, wynikające z rozszerzenia parametrów, które nie mają wartości, są usuwane.

(
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
źródło
Dobrze! Prosta i skuteczna metoda - bez potrzeby stosowania pętli bashowej i bez potrzeby wywoływania aplikacji narzędziowej - BTW. Jak wspomniałeś „(spacja)” , dla jasności wskazałbym, że działa dobrze z dowolną kombinacją znaków ogranicznika, w tym spacją.
Peter.O
W moich testach ustawienie IFS=' '(tj. Biała spacja) zachowuje się tak samo. Uważam, że jest to mniej mylące niż jawny argument zerowy („” lub „”) z IFS.
Micha Wiedenmann
To okropne rozwiązanie, jeśli dane zawierają osadzone białe znaki. To, jeśli twoje dane to „bc” zamiast „abc”, IFS = „” podzieliłby „a” na osobny element od „bc”.
Dejay Clayton
5

Z bashstrony man:

Każdy znak w IFS, który nie jest białą spacją IFS, wraz z dowolnymi sąsiadującymi znakami białych spacji IFS, ogranicza pole. Sekwencja białych znaków IFS jest również traktowana jako separator.

Oznacza to, że białe znaki IFS (spacja, tabulator i nowa linia) nie są traktowane jak inne separatory. Jeśli chcesz uzyskać dokładnie to samo zachowanie z alternatywnym separatorem, możesz wykonać zamianę separatora za pomocą trlub 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

%#%#%#%#%Rzeczą jest magiczną wartość zastąpić możliwe przestrzenie wewnątrz pola, oczekuje się, że jest „wyjątkowy” (lub bardzo unlinkely). Jeśli masz pewność, że w polach nie będzie już miejsca, po prostu upuść tę część).

jon_d
źródło
@ FussyS ... Dzięki (patrz modyfikacja w moim pytaniu) ... Być może dałeś mi odpowiedź na moje zamierzone pytanie .. i ta odpowiedź może być (prawdopodobnie jest) „Nie ma sposobu, aby IFS zachowywał się w sposób, w jaki chcę "... Zamierzam trprzykłady, aby pokazać problem ... Chcę uniknąć wywołania systemowego, więc przyjrzę się opcji bash wykraczającej poza tę, o ${var##:}której wspomniałem w komentarzu do odpowiedzi Glena ... Poczekam chwilę ... może istnieje sposób na nakłonienie IFS, w przeciwnym razie pierwsza część twojej odpowiedzi była po ...
Peter.O
To traktowanie IFSjest takie samo we wszystkich powłokach typu Bourne'a, jest określone w POSIX .
Gilles „SO- przestań być zły”
Ponad 4 lata, odkąd zadałem to pytanie - znalazłem odpowiedź @ nazada (opublikowaną ponad rok temu), że jest to najprostszy sposób na żonglowanie IFS w celu utworzenia tablicy z dowolną liczbą i kombinacją IFSznaków jako łańcucha ogranicznika. Odpowiedzi na moje pytanie najlepiej udzielił odpowiedź jon_d, ale odpowiedź @ nazada pokazuje sprytny sposób użycia IFSbez pętli i aplikacji narzędziowych.
Peter.O,
2

Ponieważ bash IFS nie zapewnia wewnętrznego sposobu traktowania kolejnych znaków delimitera jako pojedynczego separatora (dla separatorów spoza spacji), stworzyłem wersję all bash (w przeciwieństwie do używania połączenia zewnętrznego np. Tr, awk, sed )

Może obsługiwać IFS z wieloma znakami ..

Oto jego czas wykonania; wraz z podobnymi testami dla opcji tri awkpokazanymi na tej stronie pytań / odpowiedzi ... Testy są oparte na 10000 iteracjach po prostu zbudowania tablicy (bez I / O) ...

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

Oto wynik

# 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"

Oto skrypt

#!/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
źródło
Świetna robota, ciekawe +1!
F. Hauri,
1

Możesz to zrobić również z gawk, ale to nie jest ładne:

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

wyjścia

# arr[0] "abc"
# arr[1] "def"
# arr[2] "ghi"
Glenn Jackman
źródło
Dzięki ... Wydaje mi się, że nie było jasne w mojej głównej prośbie (zmodyfikowane pytanie) ... Łatwo to zrobić, zmieniając tylko $varna ${var##:}... Naprawdę szukałem sposobu na ulepszenie samego IFS .. Chcę zrobić to bez zewnętrznego wywołania (mam wrażenie, że bash może to zrobić bardziej efektywnie niż jakikolwiek zewnętrzny może .. więc będę śledził tę ścieżkę) ... twoja metoda działa (+1) .... Jak dotąd w miarę modyfikowania danych wejściowych wolałbym wypróbować to za pomocą basha, niż awk lub tr (
uniknęłoby
@fred, jak wspomniano, IFS ogranicza tylko kolejne kolejne ograniczniki dla domyślnej wartości białych znaków. W przeciwnym razie kolejne separatory powodują powstanie zbędnych pustych pól. Oczekuję, że jedno lub dwa połączenia zewnętrzne prawdopodobnie nie wpłyną w żaden sposób na wydajność.
glenn jackman
@glen .. (Powiedziałeś, że twoja odpowiedź nie jest „ładna” .. Myślę, że tak! :) Jednak stworzyłem wersję all-bash (vs wywołanie zewnętrzne) i opartą na 10000 iteracjach po prostu budowania tablicy ( nie I / O) ... bash 1.276s... call (awk) 0m32.210s,,, call (tr) 0m32.178s... Zrób to kilka razy i możesz pomyśleć, że bash jest powolny! ... Czy w tym przypadku awk jest łatwiejszy? ... nie, jeśli masz już fragment kodu :) ... opublikuję go później; Muszę już iść.
Peter.O
Tak na marginesie, to twój skrypt gawk ... Zasadniczo wcześniej nie korzystałem z awk, więc przyglądałem się temu (i innym) szczegółowo ... Nie mogę wybrać dlaczego, ale wspomnę problem w każdym razie .. Gdy podane dane cytowane, tracą cytaty i dzielą się spacjami między cytatami .. i zawieszają się na nieparzystej liczbie cytatów ... Oto dane testowe:var="The \"X\" factor:::A single '\"' crashes:::\"One Two\""
Peter.O
-1

Prosta odpowiedź brzmi: zwinąć wszystkie ograniczniki do jednego (pierwszego).
Które wymagają pętli (która działa krócej niż log(N)razy):

 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

Pozostaje tylko poprawnie podzielić ciąg na jednym separatorze i wydrukować go:

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

Nie ma potrzeby set -fani zmieniać IFS.
Testowany ze spacjami, znakami nowej linii i znakami globu. Cała praca. Dość powolny (należy się spodziewać pętli powłoki).
Ale tylko dla bash (bash 4.4+ ze względu na opcję -dreadrayray).


sh

Wersja powłoki nie może używać tablicy, jedyną dostępną tablicą są parametry pozycyjne.
Użycie tr -sjest tylko jedną linią (IFS nie zmienia się w skrypcie):

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

I wydrukuj to:

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

Wciąż powolne, ale niewiele więcej.

Polecenie commandjest nieprawidłowe w Bourne.
W zsh commandwywołuje tylko polecenia zewnętrzne i powoduje, że eval nie powiedzie się, jeśli commandzostanie użyte.
W ksh nawet przy commandwartości IFS zmienia się w zakresie globalnym.
I commandsprawia, że podział nie w mksh powiązanych muszli (mksh, lksh, posh) usunięcie polecenia commandsprawia, że uruchomienie kodu na bardziej muszli. Ale: usunięcie commandspowoduje, że IFS zachowa swoją wartość w większości powłok (eval jest specjalną wbudowaną funkcją), z wyjątkiem bash (bez trybu posix) i zsh w domyślnym trybie (bez emulacji). Nie można sprawić, aby ta koncepcja działała w domyślnym Zsh zarówno z, jak i bez command.


Wieloznakowy IFS

Tak, IFS może mieć wiele znaków, ale każdy znak wygeneruje jeden argument:

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

Wyjdzie:

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

Dzięki bash możesz pominąć to commandsłowo, jeśli nie jest emulacji sh / POSIX. Polecenie zakończy się niepowodzeniem w ksh93 (IFS zachowuje zmienioną wartość). W zsh polecenie commandpowoduje, że zsh próbuje znaleźć evaljako polecenie zewnętrzne (którego nie znajduje) i kończy się niepowodzeniem.

Dzieje się tak, ponieważ jedynymi znakami IFS, które są automatycznie zwinięte do jednego separatora, są białe znaki IFS.
Jedno pole w IFS zwinie wszystkie kolejne spacje do jednego. Jedna karta zwinie wszystkie karty. Jedna spacja i jedna karta zwiną sekwencje spacji i / lub tabulatorów do jednego separatora. Powtórz pomysł z nową linią.

Aby zwinąć kilka ograniczników, wymagana jest pewna żonglerka.
Zakładając, że ASCII 3 (0x03) nie jest używany na wejściu 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.

Większość komentarzy na temat ksh, zsh i bash (about commandi IFS) nadal obowiązuje tutaj.

Wartość $'\0'mniej prawdopodobna przy wprowadzaniu tekstu, ale zmienne bash nie mogą zawierać NULs ( 0x00).

W sh nie ma wewnętrznych poleceń do wykonania tych samych operacji na łańcuchach, więc tr jest jedynym rozwiązaniem dla skryptów sh.

Izaak
źródło
Tak, napisałem to dla powłoki, o którą OP poprosił: Bash. W tej powłoce IFS nie jest przechowywany. I tak, nie jest przenośny, na przykład dla Zsh. @ StéphaneChazelas
Isaac
W przypadku bash i zsh zachowują się tak, jak określa POSIX po wywołaniu jako sh
Stéphane Chazelas
@ StéphaneChazelas Dodano (wiele) uwag na temat ograniczeń każdej powłoki.
Izaak
@ StéphaneChazelas Dlaczego głosowanie negatywne?
Izaak
Nie wiem, to nie byłem ja. BTW, myślę, że jest tutaj dedykowane command eval
pytanie