¿Puede IFS (separador de campo interno) funcionar como un separador único para múltiples caracteres delimitadores consecutivos?

10

Analizar una matriz usando IFS con valores de espacio no blancos crea elementos vacíos.
Incluso usar tr -spara reducir múltiples delimitaciones a una sola delimitación no es suficiente.
Un ejemplo puede explicar el problema con mayor claridad.
¿Hay alguna forma de lograr resultados "normales" mediante un ajuste de IFS (¿hay una configuración asociada para cambiar el comportamiento de IFS? ... es decir, actuar de la misma manera que el espacio en blanco predeterminado 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>"

Aquí está la salida


============== 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
fuente
Hay una mejor (creo) respuesta a la misma pregunta: stackoverflow.com/a/14789518/1765658
F. Hauri

Respuestas:

3

Para eliminar múltiples caracteres delimitadores consecutivos (sin espacio), se pueden usar dos expansiones de parámetros (cadena / matriz). El truco consiste en establecer la IFSvariable en la cadena vacía para la expansión del parámetro de matriz.

Esto está documentado en man bashvirtud de la división de palabras :

Los argumentos nulos implícitos sin comillas, resultantes de la expansión de parámetros que no tienen valores, se eliminan.

(
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
fuente
¡Bueno! Un método simple y efectivo, sin necesidad de un bucle bash y sin necesidad de llamar a una aplicación de utilidad, por cierto. Como mencionó "(sin espacio)" , quisiera señalar, para mayor claridad, que funciona bien con cualquier combinación de caracteres delimitadores, incluido el espacio.
Peter
En mi configuración de pruebas IFS=' '(es decir, un espacio en blanco) se comporta igual. Esto me resulta menos confuso que un argumento nulo explícito ("" o '') de IFS.
Micha Wiedenmann
Esa es una solución terrible si sus datos contienen espacios en blanco incrustados. Esto, si sus datos eran 'a bc' en lugar de 'abc', IFS = "" dividiría 'a' en un elemento separado de 'bc'.
Dejay Clayton
5

Desde la página de bashmanual:

Cualquier carácter en IFS que no sea espacio en blanco IFS, junto con cualquier carácter de espacio en blanco IFS adyacente, delimita un campo. Una secuencia de caracteres de espacio en blanco IFS también se trata como un delimitador.

Significa que el espacio en blanco IFS (espacio, tabulación y nueva línea) no se trata como los otros separadores. Si desea obtener exactamente el mismo comportamiento con un separador alternativo, puede hacer un intercambio de separador con la ayuda de 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 es un valor mágico para reemplazar los espacios posibles dentro de los campos, se espera que sea "único" (o muy poco vinculado). Si está seguro de que nunca habrá espacio en los campos, simplemente suelte esta parte).

jon_d
fuente
@FussyS ... Gracias (vea la modificación en mi pregunta) ... Puede que me haya dado la respuesta a mi pregunta prevista ... y esa respuesta puede ser (probablemente sea) "No hay forma de que IFS se comporte en el de la manera que quiero "... Asisto a los trejemplos para mostrar el problema ... Quiero evitar una llamada al sistema, así que buscaré una opción bash más allá de la ${var##:}que mencioné en mi comentario al contestador de Glen ... Esperaré un momento ... tal vez hay una manera de convencer a IFS, de lo contrario, la primera parte de su respuesta fue después ...
Peter
Ese tratamiento IFSes el mismo en todos los shells de estilo Bourne, se especifica en POSIX .
Gilles 'SO- deja de ser malvado'
Hace más de 4 años desde que hice esta pregunta, encontré que la respuesta de @ nazad (publicada hace más de un año) es la forma más simple de hacer malabarismos con IFS para crear una matriz con cualquier número y combinación de IFScaracteres como cadena de delimitador. Mi pregunta fue mejor respondida por jon_d, pero la respuesta de @ nazad muestra una forma ingeniosa de usar IFSsin bucles ni aplicaciones de utilidad.
Peter
2

Como bash IFS no proporciona una forma interna de tratar los caracteres delimitadores consecutivos como un único delimitador (para delimitadores que no son espacios en blanco), he creado una versión de todo bash (vs.utilizando una llamada externa, por ejemplo, tr, awk, sed )

Puede manejar IFS de múltiples caracteres.

A continuación se resu su tiempo de ejecución; ts, junto con pruebas similares para el try awkopciones que aparecen en esta / Una página Q ... Las pruebas se basan en 10000 itterations de sólo la construcción de la arrray (sin E / S) ...

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

Aquí está la salida

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

Aquí está el guión

#!/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
fuente
Gran trabajo, interesante +1!
F. Hauri
1

También puedes hacerlo con gawk, pero no es bonito:

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

salidas

# arr[0] "abc"
# arr[1] "def"
# arr[2] "ghi"
Glenn Jackman
fuente
Gracias ... me parece que tienen de no haber sido claro en mi solicitud principal (pregunta modificada) ... Es bastante fácil hacerlo con sólo cambiar mi $vara ${var##:}... Yo estaba realmente tras una manera de ajustar IFS sí .. yo quiero hacer esto sin una llamada externa (tengo la sensación de que bash puede hacer esto de manera más eficiente que cualquier otra externa ... así que seguiré en ese camino) ... su método funciona (+1) ... Hasta ahora a medida que avanza la modificación de la entrada, preferiría probarlo con bash, en lugar de awk o tr (evitaría una llamada al sistema), pero realmente estoy pasando el rato con un ajuste IFS ...
Peter.O
@fred, como se mencionó, IFS solo absorbe múltiples delimitadores consecutivos para el valor predeterminado de espacio en blanco. De lo contrario, los delimitadores consecutivos resultan en campos vacíos extraños. Espero que una o dos llamadas externas sean extremadamente improbables que afecten el rendimiento de manera real.
Glenn Jackman
@glen ... (Dijiste que tu respuesta no es "bonita" ... ¡Creo que lo es! :) Sin embargo, he creado una versión completa (en comparación con una llamada externa) y en base a 10000 iteraciones de solo construir la matriz ( sin E / S) ... bash 1.276s... call (awk) 0m32.210s,,, call (tr) 0m32.178s... ¡Hazlo varias veces y podrías pensar que bash es lento! ... ¿Es awk más fácil en este caso? ... no si ya tienes el fragmento :) ... lo publicaré más tarde; debo irme ahora.
Peter
Por cierto, re su guión gawk ... Básicamente no he usado awk antes, así que lo he estado mirando (y otros) en detalle ... No puedo elegir por qué, pero mencionaré el problema de todos modos ... Cuando se le dan datos citados, pierde las comillas y se divide en espacios entre las comillas ... y se bloquea por un número impar de comillas ... Aquí están los datos de prueba:var="The \"X\" factor:::A single '\"' crashes:::\"One Two\""
Peter.O
-1

La respuesta simple es: colapsar todos los delimitadores en uno (el primero).
Eso requiere un bucle (que se ejecuta menos de log(N)veces):

 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

Todo lo que queda por hacer es dividir correctamente la cadena en un delimitador e imprimirla:

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

No es necesario set -fni cambiar IFS.
Probado con espacios, líneas nuevas y caracteres globales. Todo el trabajo. Bastante lento (como debería esperarse que sea un ciclo de shell).
Pero solo para bash (bash 4.4+ debido a la opción -dde readarray).


sh

Una versión de shell no puede usar una matriz, la única matriz disponible son los parámetros posicionales.
Usar tr -ses solo una línea (IFS no cambia en el script):

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

E imprimirlo:

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

Todavía lento, pero no mucho más.

El comando commandno es válido en Bourne.
En zsh, commandllama solo a comandos externos y hace que eval falle si commandse usa.
En ksh, incluso con command, el valor de IFS cambia en el ámbito global.
Y commandhace que la división falle en los shells relacionados con mksh (mksh, lksh, posh) Al eliminar el comando, commandel código se ejecuta en más shells. Pero: la eliminación commandhará que IFS conserve su valor en la mayoría de los shells (eval es un valor incorporado especial) excepto en bash (sin modo posix) y zsh en modo predeterminado (sin emulación). No se puede hacer que este concepto funcione en zsh predeterminado con o sin command.


Múltiples caracteres IFS

Sí, IFS podría tener varios caracteres, pero cada carácter generará un argumento:

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

Saldrá:

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

Con bash, puede omitir la commandpalabra si no está en la emulación sh / POSIX. El comando fallará en ksh93 (IFS mantiene el valor cambiado). En zsh, el comando commandhace que zsh intente buscar evalcomo un comando externo (que no encuentra) y falla.

Lo que sucede es que los únicos caracteres IFS que se contraen automáticamente en un delimitador son espacios en blanco IFS.
Un espacio en IFS colapsará todos los espacios consecutivos en uno. Una pestaña colapsará todas las pestañas. Un espacio y una pestaña contraerán series de espacios y / o pestañas en un delimitador. Repita la idea con nueva línea.

Para colapsar varios delimitadores se requieren algunos malabarismos.
Suponiendo que ASCII 3 (0x03) no se usa en la entrada 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 mayoría de los comentarios sobre ksh, zsh y bash (about commande IFS) todavía se aplican aquí.

Un valor de $'\0'sería menos probable en la entrada de texto, pero las variables bash no pueden contener NUL ( 0x00).

No hay comandos internos en sh para hacer las mismas operaciones de cadena, por lo que tr es la única solución para los scripts sh.

Isaac
fuente
Sí, escribí que para el shell el OP solicitó: Bash. En ese shell IFS no se mantiene. Y sí, no es portátil, por ejemplo, para zsh. @ StéphaneChazelas
Isaac
En el caso de bash y zsh, se comportan como POSIX especifica cuando se invoca como sh
Stéphane Chazelas
@ StéphaneChazelas Se agregaron (muchas) notas sobre las limitaciones de cada shell.
Isaac
@ StéphaneChazelas ¿Por qué el voto negativo?
Isaac
No sé, no fui yo. Por cierto, creo que hay un Q&A dedicado aquí sobre command evalIIRC por Gilles
Stéphane Chazelas