IFS (Internal Field Separator) può funzionare come un singolo separatore per più caratteri consecutivi delimitatori?


10

L'analisi di un array mediante IFS con valori di spazio non bianchi crea elementi vuoti.
Anche l'utilizzo tr -sper restringere più delimit in un singolo delim non è sufficiente.
Un esempio può spiegare il problema in modo più chiaro.
Esiste un modo per ottenere risultati "normali" tramite una modifica dell'IFS (esiste un'impostazione associata per modificare il comportamento dell'IFS? .... cioè. Agire allo stesso modo dello spazio bianco predefinito 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>"

Ecco l'output


============== 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>

C'è una risposta migliore (credo) alla stessa domanda: stackoverflow.com/a/14789518/1765658
F. Hauri,

Risposte:


3

Per rimuovere più caratteri consecutivi (non spaziali) del delimitatore, è possibile utilizzare due espansioni di parametri (stringa / matrice). Il trucco è impostare la IFSvariabile sulla stringa vuota per l'espansione dei parametri dell'array.

Questo è documentato in man bashsotto Splitting Word :

Gli argomenti null impliciti non quotati, derivanti dall'espansione di parametri che non hanno valori, vengono rimossi.

(
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
)

Buono! Un metodo semplice ed efficace - senza bisogno di un loop bash e senza bisogno di chiamare un'app di utilità - BTW. Come hai detto "(non spazio)" , vorrei sottolineare, per chiarezza, che funziona bene con qualsiasi combinazione di caratteri delimitatori, incluso lo spazio.
Peter

Nel mio test l'impostazione IFS=' '(cioè uno spazio bianco) si comporta allo stesso modo. Trovo questo meno confuso di un argomento nullo esplicito ("" o '') di IFS.
Micha Wiedenmann,

È una soluzione terribile se i tuoi dati contengono spazi bianchi incorporati. Questo, se i tuoi dati fossero 'a bc' invece di 'abc', IFS = "" avrebbe diviso 'a' in un elemento separato da 'bc'.
Dejay Clayton,

5

Dalla bashmanpage:

Qualsiasi carattere in IFS che non sia uno spazio bianco IFS, insieme a qualsiasi carattere di spazio bianco IFS adiacente, delimita un campo. Una sequenza di caratteri spazi bianchi IFS viene anche trattata come delimitatore.

Significa che gli spazi bianchi IFS (spazio, tabulazione e nuova riga) non vengono trattati come gli altri separatori. Se si desidera ottenere esattamente lo stesso comportamento con un separatore alternativo, è possibile eseguire uno scambio di separatori con l'aiuto di tro 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

La %#%#%#%#%cosa è un valore magico per sostituire i possibili spazi all'interno dei campi, ci si aspetta che sia "unico" (o molto poco logico). Se sei sicuro che non ci sarà mai spazio nei campi, lascia cadere questa parte).


@FussyS ... Grazie (vedi modifica nella mia domanda) ... Potresti avermi dato la risposta alla mia domanda voluta .. e quella risposta potrebbe essere (probabilmente lo è) "Non c'è modo di far sì che IFS si comporti nel modo che voglio "... intendo gli tresempi per mostrare il problema ... Voglio evitare una chiamata di sistema, quindi guarderò un'opzione bash oltre a quella ${var##:}che ho menzionato nel mio commento a glen's ansewer .... Aspetterò un po '.. forse c'è un modo per convincere IFS, altrimenti la prima parte della tua risposta è stata dopo ...
Peter.O

Questo trattamento IFSè lo stesso in tutte le shell in stile Bourne, è specificato in POSIX .
Gilles 'SO- smetti di essere malvagio' il

Oltre 4 anni da quando ho posto questa domanda, ho trovato la risposta di @ nazad (pubblicata più di un anno fa) come il modo più semplice di destreggiarsi tra IFS per creare un array con qualsiasi numero e combinazione di IFScaratteri come stringa delimitatore. La mia domanda è stata meglio risolta da jon_d, ma la risposta di @nazad mostra un modo elegante da usare IFSsenza loop e senza app di utilità.
Peter,

2

Poiché bash IFS non fornisce un modo interno per trattare i caratteri delimitatori consecutivi come un unico delimitatore (per i delimitatori non di spazi bianchi), ho messo insieme una versione all bash (rispetto all'uso di una chiamata esterna, ad esempio tr, awk, sed )

Può gestire IFS multi-carattere ..

Ecco i suoi risultati in termini di tempo di esecuzione, insieme a test simili per le opzioni tre awkmostrate in questa pagina Q / A ... I test si basano su 10000 iterazioni di solo costruzione dell'array (senza I / O) ...

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

Ecco l'output

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

Ecco la sceneggiatura

#!/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

Ottimo lavoro, +1 interessante!
F. Hauri,

1

Puoi farlo anche con gawk, ma non è carino:

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

uscite

# arr[0] "abc"
# arr[1] "def"
# arr[2] "ghi"

Grazie ... Mi sembra di avere non fosse stato chiaro nella mia principale richiesta (domanda modificato) ... E 'abbastanza facile per farlo, semplicemente cambiando il mio $vara ${var##:}... ero davvero dopo un modo per ritoccare la calibrazione IFS stesso .. voglio per farlo senza una chiamata esterna (ho la sensazione che bash possa farlo in modo più efficace di qualsiasi altro esterno .. quindi continuerò su quella traccia) ... il tuo metodo funziona (+1) .... Per quanto riguarda per quanto riguarda la modifica dell'input, preferirei provarlo con bash, piuttosto che awk o tr (eviterebbe una chiamata di sistema), ma sto davvero
andando in

@fred, come accennato, IFS assorbe solo più delimitatori consecutivi per il valore di spazio bianco predefinito. Altrimenti, i delimitatori consecutivi producono campi vuoti estranei. Mi aspetto che sia improbabile che una o due chiamate esterne abbiano un impatto sulle prestazioni in modo reale.
Glenn Jackman,

@glen .. (Hai detto che la tua risposta non è "carina" .. Penso che lo sia! :) Tuttavia, ho messo insieme una versione all bash (vs una chiamata esterna) e basata su 10000 iterazioni del solo costruire l'array ( no I / O) ... bash 1.276s... call (awk) 0m32.210s,,, call (tr) 0m32.178s... Fallo alcune volte e potresti pensare che bash sia lento! ... In questo caso Awk è più facile? ... non se hai già lo snippet :) ... lo posterò più tardi; devo andare adesso.
Peter

A proposito, per quanto riguarda il tuo script gawk ... In pratica non ho mai usato awk prima, quindi l'ho guardato (e altri) in dettaglio ... Non riesco a scegliere il perché, ma citerò il problema comunque .. Quando vengono dati i dati citati, perde le virgolette e si divide in spazi tra le virgolette .. e si arresta in modo var="The \"X\" factor:::A single '\"' crashes:::\"One Two\""
anomalo

-1

La semplice risposta è: comprimi tutti i delimitatori in uno (il primo).
Ciò richiede un ciclo (che funziona meno delle log(N)volte):

 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

Non resta che dividere correttamente la stringa su un delimitatore e stamparla:

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

Non è necessario set -fné modificare IFS.
Testato con spazi, newline e caratteri glob. Tutto il lavoro. Abbastanza lento (come dovrebbe essere previsto un ciclo di shell).
Ma solo per bash (bash 4.4+ a causa dell'opzione -ddi readarray).


sh

Una versione di shell non può usare un array, l'unico array disponibile sono i parametri posizionali.
L'utilizzo tr -sè solo una riga (IFS non cambia nello script):

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

E stampalo:

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

Ancora lento, ma non molto di più.

Il comando commandnon è valido in Bourne.
In zsh, commandchiama solo comandi esterni e fa fallire eval se commandviene usato.
In ksh, anche con command, il valore di IFS viene modificato nell'ambito globale.
E commandfa fallire la divisione nelle shell correlate a mksh (mksh, lksh, posh) La rimozione del comando commandfa eseguire il codice su più shell. Ma: la rimozione commandfarà in modo che IFS mantenga il suo valore nella maggior parte delle shell (eval è un builtin speciale) tranne che in bash (senza modalità posix) e zsh in modalità predefinita (nessuna emulazione). Questo concetto non può essere fatto funzionare in zsh predefinito con o senza command.


IFS a più caratteri

Sì, IFS potrebbe essere multi carattere, ma ogni personaggio genererà un argomento:

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

Uscita:

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

Con bash, puoi omettere la commandparola se non nell'emulazione sh / POSIX. Il comando fallirà in ksh93 (IFS mantiene il valore modificato). In zsh il comando commandfa sì che zsh tenti di trovare evalun comando esterno (che non trova) e non riesce.

Quello che succede è che gli unici caratteri IFS che vengono automaticamente compressi in un delimitatore sono gli spazi bianchi IFS.
Uno spazio in IFS comprime tutti gli spazi consecutivi in ​​uno. Una scheda comprime tutte le schede. Uno spazio e una scheda comprimono le serie di spazi e / o schede in un delimitatore. Ripeti l'idea con newline.

Per comprimere diversi delimitatori è necessario un po 'di giocoleria.
Supponendo che ASCII 3 (0x03) non sia utilizzato nell'input 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.

La maggior parte dei commenti su ksh, zsh e bash (about commande IFS) si applicano ancora qui.

Un valore di $'\0'sarebbe meno probabile nell'input di testo, ma le variabili bash non possono contenere NUL ( 0x00).

Non ci sono comandi interni in sh per eseguire le stesse operazioni di stringa, quindi tr è l'unica soluzione per gli script sh.


Sì, ho scritto che per la shell l'OP ha chiesto: Bash. In quella shell IFS non è mantenuto. E sì, non è portatile, ad esempio zsh. @ StéphaneChazelas
Isaac

Nel caso di bash e zsh, si comportano come POSIX specifica quando invocato come sh
Stéphane Chazelas

@ StéphaneChazelas Aggiunte (molte) note sui limiti di ogni shell.
Isaac,

@ StéphaneChazelas Perché il downvote?
Isaac,

Non lo so, non sono stato io. A proposito, penso che ci sia un D&R dedicato qui su command evalIIRC di Gilles
Stéphane Chazelas
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.