¿Cómo divido una cadena en un delimitador en Bash?


2043

Tengo esta cadena almacenada en una variable:

IN="bla@some.com;john@home.com"

Ahora me gustaría dividir las cadenas por ;delimitador para que tenga:

ADDR1="bla@some.com"
ADDR2="john@home.com"

No necesariamente necesito las variables ADDR1y ADDR2. Si son elementos de una matriz, eso es aún mejor.


Después de las sugerencias de las respuestas a continuación, terminé con lo siguiente, que es lo que estaba buscando:

#!/usr/bin/env bash

IN="bla@some.com;john@home.com"

mails=$(echo $IN | tr ";" "\n")

for addr in $mails
do
    echo "> [$addr]"
done

Salida:

> [bla@some.com]
> [john@home.com]

Había una solución que implicaba configurar Internal_field_separator (IFS) en ;. No estoy seguro de lo que sucedió con esa respuesta, ¿cómo restablece los IFSvalores predeterminados?

RE: IFSsolución, probé esto y funciona, conservo el viejo IFSy luego lo restauro:

IN="bla@some.com;john@home.com"

OIFS=$IFS
IFS=';'
mails2=$IN
for x in $mails2
do
    echo "> [$x]"
done

IFS=$OIFS

Por cierto, cuando intenté

mails2=($IN)

Solo obtuve la primera cadena cuando la imprimí en bucle, sin corchetes alrededor $INfunciona.


14
Con respecto a su "Edit2": puede simplemente "desarmar IFS" y volverá al estado predeterminado. No es necesario guardarlo y restaurarlo explícitamente a menos que tenga alguna razón para esperar que ya se haya establecido en un valor no predeterminado. Además, si está haciendo esto dentro de una función (y, si no lo está, ¿por qué no?), Puede establecer IFS como una variable local y volverá a su valor anterior una vez que salga de la función.
Brooks Moses

19
@BrooksMoses: (a) +1 para usar local IFS=...cuando sea posible; (b) -1 para unset IFS, esto no restablece exactamente IFS a su valor predeterminado, aunque creo que un IFS no configurado se comporta igual que el valor predeterminado de IFS ($ '\ t \ n'), sin embargo, parece una mala práctica supongamos ciegamente que su código nunca será invocado con IFS configurado en un valor personalizado; (c) otra idea es invocar una subshell: (IFS=$custom; ...)cuando la subshell salga, IFS volverá a lo que era originalmente.
dubiousjim

Solo quiero echar un vistazo rápido a los caminos para decidir dónde lanzar un ejecutable, así que recurrí a correr ruby -e "puts ENV.fetch('PATH').split(':')". Si quieres mantenerte puro, bash no ayudará, pero usar cualquier lenguaje de script que tenga una división incorporada es más fácil.
nicooga

44
for x in $(IFS=';';echo $IN); do echo "> [$x]"; done
user2037659

2
Para guardarlo como una matriz, tuve que colocar otro paréntesis y cambiarlo \nsolo por un espacio. Entonces la línea final es mails=($(echo $IN | tr ";" " ")). Así que ahora puedo verificar los elementos mailsutilizando la notación de matriz mails[index]o simplemente iterando en un bucle
después del

Respuestas:


1236

Puede establecer la variable del separador de campo interno (IFS) y luego dejar que se analice en una matriz. Cuando esto sucede en un comando, la asignación a IFSsolo tiene lugar en el entorno de ese comando único (a read). Luego analiza la entrada de acuerdo con el IFSvalor de la variable en una matriz, que luego podemos iterar.

IFS=';' read -ra ADDR <<< "$IN"
for i in "${ADDR[@]}"; do
    # process "$i"
done

Analizará una línea de elementos separados por ;, empujándolo en una matriz. Cosas para procesar todo $IN, cada vez una línea de entrada separada por ;:

 while IFS=';' read -ra ADDR; do
      for i in "${ADDR[@]}"; do
          # process "$i"
      done
 done <<< "$IN"

22
Esta es probablemente la mejor manera. ¿Cuánto tiempo persistirá IFS en su valor actual, puede estropear mi código al configurarlo cuando no debería ser, y cómo puedo restablecerlo cuando haya terminado?
Chris Lutz

77
ahora, después de que se haya aplicado la corrección, solo dentro de la duración del comando de lectura :)
Johannes Schaub - litb

14
Puede leer todo de una vez sin usar un ciclo while: leer -r -d '' -a addr <<< "$ in" # La -d '' es clave aquí, le dice a read que no se detenga en la primera línea nueva ( que es el valor predeterminado -d) pero para continuar hasta EOF o un byte NULL (que solo ocurre en datos binarios).
lhunath

55
@LucaBorrione Configurando IFSen la misma línea que readsin punto y coma u otro separador, en lugar de en un comando separado, lo aplica a ese comando, por lo que siempre se "restaura"; No necesita hacer nada manualmente.
Charles Duffy

55
@imagineerThis Hay un error que involucra herejías y cambios locales en IFS que requiere $INser citado. El error se corrigió en bash4.3.
chepner

973

Tomado de la matriz dividida de script de shell Bash :

IN="bla@some.com;john@home.com"
arrIN=(${IN//;/ })

Explicación:

Esta construcción reemplaza todas las ocurrencias de ';'(la inicial //significa reemplazo global) en la cadena INcon ' '(un solo espacio), luego interpreta la cadena delimitada por espacios como una matriz (eso es lo que hacen los paréntesis circundantes).

La sintaxis utilizada dentro de las llaves para reemplazar cada ';'carácter con un ' 'carácter se llama Expansión de parámetros .

Hay algunas trampas comunes:

  1. Si la cadena original tiene espacios, necesitará usar IFS :
    • IFS=':'; arrIN=($IN); unset IFS;
  2. Si la cadena original tiene espacios y el delimitador es una nueva línea, puede establecer IFS con:
    • IFS=$'\n'; arrIN=($IN); unset IFS;

84
Solo quiero agregar: este es el más simple de todos, puede acceder a los elementos de la matriz con $ {arrIN [1]} (a partir de ceros, por supuesto)
Oz123

26
Encontrado: la técnica de modificar una variable dentro de $ {} se conoce como 'expansión de parámetros'.
KomodoDave

23
No, no creo que esto funcione cuando también hay espacios presentes ... está convirtiendo ',' a '' y luego construyendo una matriz separada por espacios.
Ethan

12
Muy conciso, pero hay advertencias para uso general : el shell aplica división de palabras y expansiones a la cadena, que pueden ser indeseadas; solo pruébalo con. IN="bla@some.com;john@home.com;*;broken apart". En resumen: este enfoque se romperá si sus tokens contienen espacios y / o caracteres incrustados. como *que sucede que un token coincida con los nombres de archivo en la carpeta actual.
mklement0

53
Este es un mal enfoque por otras razones: por ejemplo, si su cadena contiene ;*;, entonces *se expandirá a una lista de nombres de archivo en el directorio actual. -1
Charles Duffy

249

Si no te importa procesarlos de inmediato, me gusta hacer esto:

for i in $(echo $IN | tr ";" "\n")
do
  # process
done

Podría usar este tipo de bucle para inicializar una matriz, pero probablemente haya una forma más fácil de hacerlo. Sin embargo, espero que esto ayude.


Deberías haber mantenido la respuesta IFS. Me enseñó algo que no sabía, y definitivamente formó una matriz, mientras que esto solo es un sustituto barato.
Chris Lutz

Veo. Sí, descubro que haciendo estos experimentos tontos, voy a aprender cosas nuevas cada vez que intento responder. He editado cosas basadas en comentarios de #bash IRC y no borrado :)
Johannes Schaub - litb

33
-1, obviamente no eres consciente de la división de palabras, porque está introduciendo dos errores en tu código. uno es cuando no cotiza $ IN y el otro es cuando finge que una nueva línea es el único delimitador utilizado en la división de palabras. Está iterando sobre cada PALABRA en IN, no en cada línea, y DEFINITIVAMENTE no en todos los elementos delimitados por un punto y coma, aunque puede parecer que tiene el efecto secundario de parecer que funciona.
lhunath

3
Puede cambiarlo para que haga eco de "$ IN" | tr ';' '\ n' | mientras lee -r ADDY; # proceso "$ ADDY"; hecho para darle suerte, creo :) Tenga en cuenta que esto se bifurcará, y no puede cambiar las variables externas desde dentro del bucle (es por eso que usé la sintaxis <<< "$ IN") entonces
Johannes Schaub - litb

8
Para resumir el debate en los comentarios: Advertencias para uso general : el shell aplica división de palabras y expansiones a la cadena, que puede no ser deseada; solo pruébalo con. IN="bla@some.com;john@home.com;*;broken apart". En resumen: este enfoque se romperá si sus tokens contienen espacios y / o caracteres incrustados. como *que sucede que un token coincida con los nombres de archivo en la carpeta actual.
mklement0

202

Respuesta compatible

Hay muchas formas diferentes de hacer esto en .

Sin embargo, es importante tener en cuenta que bashtiene muchas características especiales (los llamados bashismos ) que no funcionarán en ningún otro.

En particular, las matrices , las matrices asociativas y la sustitución de patrones , que se usan en las soluciones en esta publicación, así como en otras en el hilo, son bashismos y pueden no funcionar bajo otras capas que muchas personas usan.

Por ejemplo: en mi Debian GNU / Linux , hay un shell estándar llamado; Conozco a muchas personas que les gusta usar otro shell llamado; y también hay una herramienta especial llamada con su propio intérprete de shell ()

Cadena solicitada

La cadena que se dividirá en la pregunta anterior es:

IN="bla@some.com;john@home.com"

Usaré una versión modificada de esta cadena para asegurarme de que mi solución sea robusta para las cadenas que contienen espacios en blanco, lo que podría romper otras soluciones:

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"

División de cadena basada en delimitador en (versión> = 4.2)

En puro bash , podemos crear una matriz con elementos divididos por un valor temporal para IFS (el separador de campo de entrada ). El IFS, entre otras cosas, le dice bashqué carácter (s) debe tratar como un delimitador entre elementos al definir una matriz:

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"

# save original IFS value so we can restore it later
oIFS="$IFS"
IFS=";"
declare -a fields=($IN)
IFS="$oIFS"
unset oIFS

En las versiones más recientes de bash, el prefijo de un comando con una definición de IFS cambia el IFS para ese comando solamente y lo restablece al valor anterior inmediatamente después. Esto significa que podemos hacer lo anterior en una sola línea:

IFS=\; read -a fields <<<"$IN"
# after this command, the IFS resets back to its previous value (here, the default):
set | grep ^IFS=
# IFS=$' \t\n'

Podemos ver que la cadena INse ha almacenado en una matriz llamada fields, dividida en punto y coma:

set | grep ^fields=\\\|^IN=
# fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name <fulnam@other.org>")
# IN='bla@some.com;john@home.com;Full Name <fulnam@other.org>'

(También podemos mostrar el contenido de estas variables usando declare -p:)

declare -p IN fields
# declare -- IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
# declare -a fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name <fulnam@other.org>")

Tenga en cuenta que reades la forma más rápida de hacer la división porque no se llaman tenedores o recursos externos.

Una vez que se define la matriz, puede usar un bucle simple para procesar cada campo (o, más bien, cada elemento de la matriz que ha definido ahora):

# `"${fields[@]}"` expands to return every element of `fields` array as a separate argument
for x in "${fields[@]}" ;do
    echo "> [$x]"
    done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

O bien, puede eliminar cada campo de la matriz después de procesar utilizando un enfoque de desplazamiento , que me gusta:

while [ "$fields" ] ;do
    echo "> [$fields]"
    # slice the array 
    fields=("${fields[@]:1}")
    done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Y si solo desea una impresión simple de la matriz, ni siquiera necesita recorrerla:

printf "> [%s]\n" "${fields[@]}"
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Actualización: reciente > = 4.4

En las versiones más recientes de bash, también puedes jugar con el comando mapfile:

mapfile -td \; fields < <(printf "%s\0" "$IN")

¡Esta sintaxis conserva caracteres especiales, líneas nuevas y campos vacíos!

Si no desea incluir campos vacíos, puede hacer lo siguiente:

mapfile -td \; fields <<<"$IN"
fields=("${fields[@]%$'\n'}")   # drop '\n' added by '<<<'

Con mapfile, también puede omitir la declaración de una matriz y "bucle" implícitamente sobre los elementos delimitados, llamando a una función en cada uno:

myPubliMail() {
    printf "Seq: %6d: Sending mail to '%s'..." $1 "$2"
    # mail -s "This is not a spam..." "$2" </path/to/body
    printf "\e[3D, done.\n"
}

mapfile < <(printf "%s\0" "$IN") -td \; -c 1 -C myPubliMail

(Nota: el \0final de la cadena de formato es inútil si no le interesan los campos vacíos al final de la cadena o no están presentes).

mapfile < <(echo -n "$IN") -td \; -c 1 -C myPubliMail

# Seq:      0: Sending mail to 'bla@some.com', done.
# Seq:      1: Sending mail to 'john@home.com', done.
# Seq:      2: Sending mail to 'Full Name <fulnam@other.org>', done.

O podría usar <<<, y en el cuerpo de la función incluir algún procesamiento para eliminar la nueva línea que agrega:

myPubliMail() {
    local seq=$1 dest="${2%$'\n'}"
    printf "Seq: %6d: Sending mail to '%s'..." $seq "$dest"
    # mail -s "This is not a spam..." "$dest" </path/to/body
    printf "\e[3D, done.\n"
}

mapfile <<<"$IN" -td \; -c 1 -C myPubliMail

# Renders the same output:
# Seq:      0: Sending mail to 'bla@some.com', done.
# Seq:      1: Sending mail to 'john@home.com', done.
# Seq:      2: Sending mail to 'Full Name <fulnam@other.org>', done.

División de cadena basada en delimitador en

Si no puede usar bash, o si desea escribir algo que pueda usarse en muchos shells diferentes, a menudo no puede usar bashisms , y esto incluye los arreglos que hemos estado usando en las soluciones anteriores.

Sin embargo, no necesitamos usar matrices para recorrer los "elementos" de una cadena. Hay una sintaxis utilizada en muchos shells para eliminar subcadenas de una cadena desde la primera o la última aparición de un patrón. Tenga en cuenta que *es un comodín que significa cero o más caracteres:

(La falta de este enfoque en cualquier solución publicada hasta ahora es la razón principal por la que escribo esta respuesta;)

${var#*SubStr}  # drops substring from start of string up to first occurrence of `SubStr`
${var##*SubStr} # drops substring from start of string up to last occurrence of `SubStr`
${var%SubStr*}  # drops substring from last occurrence of `SubStr` to end of string
${var%%SubStr*} # drops substring from first occurrence of `SubStr` to end of string

Según lo explicado por Score_Under :

#y %elimine la subcadena coincidente más corta posible desde el inicio y el final de la cadena respectivamente, y

##y %%elimine la subcadena coincidente más larga posible.

Usando la sintaxis anterior, podemos crear un enfoque donde extraemos "elementos" de la subcadena de la cadena eliminando las subcadenas hasta o después del delimitador.

El siguiente bloque de código funciona bien en (incluidos los Mac OS bash),, y 's :

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
while [ "$IN" ] ;do
    # extract the substring from start of string up to delimiter.
    # this is the first "element" of the string.
    iter=${IN%%;*}
    echo "> [$iter]"
    # if there's only one element left, set `IN` to an empty string.
    # this causes us to exit this `while` loop.
    # else, we delete the first "element" of the string from IN, and move onto the next.
    [ "$IN" = "$iter" ] && \
        IN='' || \
        IN="${IN#*;}"
  done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

¡Que te diviertas!


15
Los #, ##, %, y %%sustituciones tienen lo que es la OMI una explicación más fácil de recordar (por lo mucho que eliminar): #y %eliminar la cadena coincidente más corto posible, y ##y %%eliminar lo más largos posibles.
Score_Under

1
La IFS=\; read -a fields <<<"$var"falla en los saltos de línea y añadir un salto de línea final. La otra solución elimina un campo vacío final.
Isaac

El delimitador de concha es la respuesta más elegante, punto.
Eric Chen

¿Podría usarse la última alternativa con una lista de separadores de campo establecidos en otro lugar? Por ejemplo, me refiero a usar esto como un script de shell y pasar una lista de separadores de campo como parámetro posicional.
sancho.s ReinstateMonicaCellio

Sí, en un bucle:for sep in "#" "ł" "@" ; do ... var="${var#*$sep}" ...
F. Hauri

184

He visto un par de respuestas que hacen referencia al cutcomando, pero todas han sido eliminadas. Es un poco extraño que nadie haya explicado eso, porque creo que es uno de los comandos más útiles para hacer este tipo de cosas, especialmente para analizar archivos de registro delimitados.

En el caso de dividir este ejemplo específico en una matriz de script bash, trprobablemente sea más eficiente, pero cutse puede usar y es más efectivo si desea extraer campos específicos del medio.

Ejemplo:

$ echo "bla@some.com;john@home.com" | cut -d ";" -f 1
bla@some.com
$ echo "bla@some.com;john@home.com" | cut -d ";" -f 2
john@home.com

Obviamente, puede poner eso en un bucle e iterar el parámetro -f para extraer cada campo de forma independiente.

Esto se vuelve más útil cuando tiene un archivo de registro delimitado con filas como esta:

2015-04-27|12345|some action|an attribute|meta data

cutes muy útil poder acceder a cateste archivo y seleccionar un campo en particular para su posterior procesamiento.


66
Felicitaciones por usar cut, ¡es la herramienta adecuada para el trabajo! Mucho más claro que cualquiera de esos piratas informáticos.
MisterMiyagi

44
Este enfoque solo funcionará si conoce el número de elementos de antemano; necesitaría programar algo más de lógica a su alrededor. También ejecuta una herramienta externa para cada elemento.
uli42

Exactamente lo que estaba buscando para tratar de evitar una cadena vacía en un csv. Ahora también puedo señalar el valor exacto de la 'columna'. Trabajar con IFS ya utilizado en un bucle. Mejor de lo esperado para mi situación.
Louis Loudog Trottier

Muy útil para extraer ID y PID también, es decir
Milos Grujic

Vale la pena desplazarse por esta respuesta más de media página :)
Gucu112

124

Esto funcionó para mí:

string="1;2"
echo $string | cut -d';' -f1 # output is 1
echo $string | cut -d';' -f2 # output is 2

1
Aunque solo funciona con un delimitador de un solo carácter, eso es lo que estaba buscando el OP (registros delimitados por un punto y coma).
GuyPaddock

Respondió hace unos cuatro años por @Ashok , y también, hace más de un año por @DougW , que su respuesta, con aún más información. Por favor, publique una solución diferente a la de los demás.
MAChitgarha

90

¿Qué tal este enfoque:

IN="bla@some.com;john@home.com" 
set -- "$IN" 
IFS=";"; declare -a Array=($*) 
echo "${Array[@]}" 
echo "${Array[0]}" 
echo "${Array[1]}" 

Fuente


77
+1 ... pero no diría que la variable "Array" ... mascota, supongo. Buena solución.
Yzmir Ramirez

14
+1 ... pero el "set" y declarar -a son innecesarios. También podría haber usado soloIFS";" && Array=($IN)
ata

+1 Solo una nota al margen: ¿no debería ser recomendable mantener el viejo IFS y luego restaurarlo? (como lo muestra stefanB en su edición3) las personas que aterrizan aquí (a veces simplemente copiando y pegando una solución) podrían no pensar en esto
Luca Borrione

66
-1: Primero, @ata tiene razón en que la mayoría de los comandos en esto no hacen nada. En segundo lugar, utiliza la división de palabras para formar la matriz, y no hace nada para inhibir la expansión glob al hacerlo (por lo tanto, si tiene caracteres glob en cualquiera de los elementos de la matriz, esos elementos se reemplazan con nombres de archivo coincidentes).
Charles Duffy el

1
Es mejor utilizar $'...': IN=$'bla@some.com;john@home.com;bet <d@\ns* kl.com>'. Luego echo "${Array[2]}"imprimirá una cadena con nueva línea. set -- "$IN"También es necesario en este caso. Sí, para evitar la expansión global, la solución debe incluir set -f.
John_West

79

Creo que AWK es el comando mejor y eficiente para resolver su problema. AWK se incluye por defecto en casi todas las distribuciones de Linux.

echo "bla@some.com;john@home.com" | awk -F';' '{print $1,$2}'

daré

bla@some.com john@home.com

Por supuesto, puede almacenar cada dirección de correo electrónico redefiniendo el campo de impresión awk.


3
O incluso más simple: echo "bla@some.com; john@home.com" | awk 'BEGIN {RS = ";"} {print}'
Jaro

@Jaro Esto funcionó perfectamente para mí cuando tenía una cadena con comas y necesitaba volver a formatearla en líneas. Gracias.
Aquarelle

Funcionó en este escenario -> "echo" $ SPLIT_0 "| awk -F 'inode =' '{print $ 1}'"! Tuve problemas al intentar usar atrings ("inode =") en lugar de caracteres (";"). ¡$ 1, $ 2, $ 3, $ 4 se establecen como posiciones en una matriz! Si hay una manera de configurar una matriz ... ¡mejor! ¡Gracias!
Eduardo Lucio

@EduardoLucio, lo que estoy pensando es tal vez usted puede reemplazar su primer delimitador inode=en ;por ejemplo sed -i 's/inode\=/\;/g' your_file_to_process, a continuación, definir -F';'cuando se aplique awk, la esperanza de que pueda ayudar.
Tong

66
echo "bla@some.com;john@home.com" | sed -e 's/;/\n/g'
bla@some.com
john@home.com

44
-1 ¿y si la cadena contiene espacios? por ejemplo IN="this is first line; this is second line" arrIN=( $( echo "$IN" | sed -e 's/;/\n/g' ) ), producirá una matriz de 8 elementos en este caso (un elemento para cada espacio de palabras separado), en lugar de 2 (un elemento para cada línea separada por punto y coma)
Luca Borrione

3
@Luca No, el script sed crea exactamente dos líneas. Lo que crea las entradas múltiples para usted es cuando se pone en una matriz de fiesta (que divide el espacio en blanco por defecto)
Lothar

Ese es exactamente el punto: el OP necesita almacenar entradas en una matriz para recorrerlo, como puede ver en sus ediciones. Creo que su (buena) respuesta omitió mencionar para usar arrIN=( $( echo "$IN" | sed -e 's/;/\n/g' ) )para lograr eso, y aconsejar cambiar IFS IFS=$'\n'para aquellos que aterrizarán aquí en el futuro y necesitan dividir una cadena que contenga espacios. (y para restaurarlo luego). :)
Luca Borrione

1
@Luca Buen punto. Sin embargo, la asignación de matriz no estaba en la pregunta inicial cuando escribí esa respuesta.
lothar

65

Esto también funciona:

IN="bla@some.com;john@home.com"
echo ADD1=`echo $IN | cut -d \; -f 1`
echo ADD2=`echo $IN | cut -d \; -f 2`

Tenga cuidado, esta solución no siempre es correcta. En caso de que pase "bla@some.com" solamente, lo asignará a ADD1 y ADD2.


1
Puede utilizar -s para evitar el problema mencionado: superuser.com/questions/896800/... "-f, --fields = lista de selección sólo estos campos; también imprimir cualquier línea que no contiene ningún carácter delimitador, a menos que la opción -s es especificado "
fersarr

34

Una versión diferente de la respuesta de Darron , así es como lo hago:

IN="bla@some.com;john@home.com"
read ADDR1 ADDR2 <<<$(IFS=";"; echo $IN)

¡Creo que sí! Ejecute los comandos anteriores y luego "echo $ ADDR1 ... $ ADDR2" y obtendré la salida "bla@some.com ... john@home.com"
nickjb

1
Esto funcionó REALMENTE bien para mí ... Lo usé para iterar sobre una serie de cadenas que contenían datos DB, SERVER, PORT separados por comas para usar mysqldump.
Nick

55
Diagnóstico: la IFS=";"asignación existe solo en la $(...; echo $IN)subshell; Es por eso que algunos lectores (incluyéndome a mí) inicialmente piensan que no funcionará. Supuse que ADDR1 estaba gastando todo $ IN. Pero nickjb es correcto; funciona La razón es que el echo $INcomando analiza sus argumentos usando el valor actual de $ IFS, pero luego los repite a stdout usando un delimitador de espacio, independientemente de la configuración de $ IFS. Por lo tanto, el efecto neto es como si uno hubiera llamado read ADDR1 ADDR2 <<< "bla@some.com john@home.com"(tenga en cuenta que la entrada está separada por espacios, no por separado).
dubiousjim

1
Esta falla en espacios y saltos de línea, y también ampliar los comodines *en el echo $INcon una expansión de variables sin comillas.
Isaac

Realmente me gusta esta solución. Una descripción de por qué funciona sería muy útil y sería una mejor respuesta general.
Michael Gaskill

32

En Bash, una forma a prueba de balas, que funcionará incluso si su variable contiene nuevas líneas:

IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")

Mira:

$ in=$'one;two three;*;there is\na newline\nin this field'
$ IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")
$ declare -p array
declare -a array='([0]="one" [1]="two three" [2]="*" [3]="there is
a newline
in this field")'

El truco para que esto funcione es usar la -dopción de read(delimitador) con un delimitador vacío, por lo que readse ve obligado a leer todo lo que se alimenta. Y nos alimentamos readcon exactamente el contenido de la variable in, sin nueva línea final gracias a printf. Tenga en cuenta que también estamos colocando el delimitador printfpara garantizar que la cadena que se pasa readtiene un delimitador final. Sin él, readse recortarían los posibles campos vacíos finales:

$ in='one;two;three;'    # there's an empty field
$ IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")
$ declare -p array
declare -a array='([0]="one" [1]="two" [2]="three" [3]="")'

se conserva el campo vacío final.


Actualización para Bash≥4.4

Desde Bash 4.4, el incorporado mapfile(también conocido como readarray) admite la -dopción de especificar un delimitador. Por lo tanto, otra forma canónica es:

mapfile -d ';' -t array < <(printf '%s;' "$in")

55
Lo encontré como la solución rara en esa lista que funciona correctamente con \nespacios y *simultáneamente. Además, no hay bucles; La variable de matriz es accesible en el shell después de la ejecución (al contrario de la respuesta más votada). Tenga en in=$'...'cuenta que no funciona con comillas dobles. Creo que necesita más votos a favor.
John_West

28

¿Qué tal este revestimiento, si no está utilizando matrices:

IFS=';' read ADDR1 ADDR2 <<<$IN

Considere usar read -r ...para asegurarse de que, por ejemplo, los dos caracteres "\ t" en la entrada terminen como los mismos dos caracteres en sus variables (en lugar de una sola pestaña).
dubiousjim

-1 Esto no funciona aquí (ubuntu 12.04). Agregar echo "ADDR1 $ADDR1"\n echo "ADDR2 $ADDR2"a su fragmento generará ADDR1 bla@some.com john@home.com\nADDR2(\ n es nueva línea)
Luca Borrione

Esto probablemente se deba a un error que involucra IFSy aquí cadenas que se corrigieron en bash4.3. Las citas $INdeberían arreglarlo. (En teoría, $INno está sujeto a la división de palabras o al engrosamiento después de que se expande, lo que significa que las citas deberían ser innecesarias. Sin embargo, incluso en 4.3, queda al menos un error, informado y programado para ser corregido, por lo que las citas siguen siendo buenas idea.)
chepner

Esto se rompe si $ in contiene nuevas líneas incluso si se cita $ IN. Y agrega una nueva línea final.
Isaac

Un problema con esto, y con muchas otras soluciones, también es que supone que EXACTAMENTE DOS elementos en $ IN - O que está dispuesto a que el segundo y los siguientes elementos se unan en ADDR2. Entiendo que esto cumple con la pregunta, pero es una bomba de tiempo.
Steven el fácilmente divertido

23

Sin configurar el IFS

Si solo tiene un colon, puede hacer eso:

a="foo:bar"
b=${a%:*}
c=${a##*:}

conseguirás:

b = foo
c = bar

20

Aquí hay un 3-liner limpio:

in="foo@bar;bizz@buzz;fizz@buzz;buzz@woof"
IFS=';' list=($in)
for item in "${list[@]}"; do echo $item; done

donde IFSdelimita palabras basadas en el separador y ()se usa para crear una matriz . Luego [@]se utiliza para devolver cada elemento como una palabra separada.

Si tiene algún código después de eso, también necesita restaurar $IFS, por ejemplo unset IFS.


55
El uso de sin $incomillas permite expandir los comodines.
Isaac

10

La siguiente función Bash / zsh divide su primer argumento en el delimitador dado por el segundo argumento:

split() {
    local string="$1"
    local delimiter="$2"
    if [ -n "$string" ]; then
        local part
        while read -d "$delimiter" part; do
            echo $part
        done <<< "$string"
        echo $part
    fi
}

Por ejemplo, el comando

$ split 'a;b;c' ';'

rendimientos

a
b
c

Esta salida puede, por ejemplo, ser canalizada a otros comandos. Ejemplo:

$ split 'a;b;c' ';' | cat -n
1   a
2   b
3   c

En comparación con las otras soluciones dadas, esta tiene las siguientes ventajas:

  • IFSno se anula: debido al alcance dinámico de incluso las variables locales, la anulación de IFSun bucle hace que el nuevo valor se filtre en las llamadas a funciones realizadas desde dentro del bucle.

  • Las matrices no se usan: leer una cadena en una matriz usando readrequiere la bandera -aen Bash y -Aen zsh.

Si lo desea, la función se puede poner en un script de la siguiente manera:

#!/usr/bin/env bash

split() {
    # ...
}

split "$@"

No parece funcionar con delimitadores de más de 1 carácter: split = $ (split "$ content" "file: //")
madprops

Cierto - de help read:-d delim continue until the first character of DELIM is read, rather than newline
Halle Knast

8

puedes aplicar awk a muchas situaciones

echo "bla@some.com;john@home.com"|awk -F';' '{printf "%s\n%s\n", $1, $2}'

también puedes usar esto

echo "bla@some.com;john@home.com"|awk -F';' '{print $1,$2}' OFS="\n"

7

Hay una manera simple e inteligente como esta:

echo "add:sfff" | xargs -d: -i  echo {}

Pero debe usar gnu xargs, BSD xargs no puede admitir -d delim. Si usas apple mac como yo. Puede instalar gnu xargs:

brew install findutils

entonces

echo "add:sfff" | gxargs -d: -i  echo {}

4

Esta es la forma más sencilla de hacerlo.

spo='one;two;three'
OIFS=$IFS
IFS=';'
spo_array=($spo)
IFS=$OIFS
echo ${spo_array[*]}

4

Aquí hay algunas respuestas geniales (errator esp.), Pero para algo similar a dividirse en otros idiomas, que es lo que entendí que significaba la pregunta original, me decidí por esto:

IN="bla@some.com;john@home.com"
declare -a a="(${IN/;/ })";

Ahora ${a[0]}, ${a[1]}etc., son como cabría esperar. Usar ${#a[*]}para varios términos. O para iterar, por supuesto:

for i in ${a[*]}; do echo $i; done

NOTA IMPORTANTE:

Esto funciona en casos donde no hay espacios de los que preocuparse, lo que resolvió mi problema, pero puede que no resuelva el tuyo. Vaya con la $IFS(s) solución (es) en ese caso.


No funciona cuando INcontiene más de dos direcciones de correo electrónico. Consulte la misma idea (pero fija) en la respuesta de palindrom
olibre

Mejor uso ${IN//;/ }(doble barra) para que también funcione con más de dos valores. Tenga en cuenta que cualquier comodín ( *?[) se expandirá. Y un campo vacío final será descartado.
Isaac

3
IN="bla@some.com;john@home.com"
IFS=';'
read -a IN_arr <<< "${IN}"
for entry in "${IN_arr[@]}"
do
    echo $entry
done

Salida

bla@some.com
john@home.com

Sistema: Ubuntu 12.04.1


IFS no se está configurando en el contexto específico de readaquí y, por lo tanto, puede alterar el resto del código, si corresponde.
codeforester

2

Si no hay espacio, ¿por qué no esto?

IN="bla@some.com;john@home.com"
arr=(`echo $IN | tr ';' ' '`)

echo ${arr[0]}
echo ${arr[1]}

2

Use el setincorporado para cargar la $@matriz:

IN="bla@some.com;john@home.com"
IFS=';'; set $IN; IFS=$' \t\n'

Entonces, que comience la fiesta:

echo $#
for a; do echo $a; done
ADDR1=$1 ADDR2=$2

Mejor uso set -- $INpara evitar algunos problemas con "$ IN" que comienza con el guión. Aún así, la expansión sin comillas de $INexpandirá comodines ( *?[).
Isaac

2

Dos alternativas de bourne-ish donde ninguna requiere matrices bash:

Caso 1 : manténgalo simple y agradable: utilice una nueva línea como separador de registros ... por ejemplo.

IN="bla@some.com
john@home.com"

while read i; do
  # process "$i" ... eg.
    echo "[email:$i]"
done <<< "$IN"

Nota: en este primer caso, no se bifurca ningún subproceso para ayudar con la manipulación de la lista.

Idea: Quizás valga la pena usar NL de manera extensiva internamente , y solo convertirlo a un RS diferente al generar el resultado final externamente .

Caso 2 : Usando un ";" como un separador de registros ... por ejemplo.

NL="
" IRS=";" ORS=";"

conv_IRS() {
  exec tr "$1" "$NL"
}

conv_ORS() {
  exec tr "$NL" "$1"
}

IN="bla@some.com;john@home.com"
IN="$(conv_IRS ";" <<< "$IN")"

while read i; do
  # process "$i" ... eg.
    echo -n "[email:$i]$ORS"
done <<< "$IN"

En ambos casos, una sublista puede estar compuesta dentro del ciclo es persistente después de que el ciclo se haya completado. Esto es útil al manipular listas en la memoria, en lugar de almacenar listas en archivos. {ps mantén la calma y continúa B-)}


2

Además de las fantásticas respuestas que ya se proporcionaron, si solo se trata de imprimir los datos que puede considerar usar awk:

awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN"

Esto establece el separador de campo en ;, para que pueda recorrer los campos con un forbucle e imprimir en consecuencia.

Prueba

$ IN="bla@some.com;john@home.com"
$ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN"
> [bla@some.com]
> [john@home.com]

Con otra entrada:

$ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "a;b;c   d;e_;f"
> [a]
> [b]
> [c   d]
> [e_]
> [f]

2

En el shell de Android, la mayoría de los métodos propuestos simplemente no funcionan:

$ IFS=':' read -ra ADDR <<<"$PATH"                             
/system/bin/sh: can't create temporary file /sqlite_stmt_journals/mksh.EbNoR10629: No such file or directory

Lo que funciona es:

$ for i in ${PATH//:/ }; do echo $i; done
/sbin
/vendor/bin
/system/sbin
/system/bin
/system/xbin

donde //significa reemplazo global.


1
Falla si alguna parte de $ PATH contiene espacios (o nuevas líneas). También expande los comodines (asterisco *, signo de interrogación? Y llaves [...]).
Isaac

2
IN='bla@some.com;john@home.com;Charlie Brown <cbrown@acme.com;!"#$%&/()[]{}*? are no problem;simple is beautiful :-)'
set -f
oldifs="$IFS"
IFS=';'; arrayIN=($IN)
IFS="$oldifs"
for i in "${arrayIN[@]}"; do
echo "$i"
done
set +f

Salida:

bla@some.com
john@home.com
Charlie Brown <cbrown@acme.com
!"#$%&/()[]{}*? are no problem
simple is beautiful :-)

Explicación: La asignación simple usando paréntesis () convierte la lista separada por punto y coma en una matriz siempre que tenga IFS correcto mientras lo hace. El bucle FOR estándar maneja elementos individuales en esa matriz como de costumbre. Tenga en cuenta que la lista dada para la variable IN debe ser "dura", es decir, con marcas simples.

IFS debe guardarse y restaurarse ya que Bash no trata una tarea de la misma manera que un comando. Una solución alternativa es envolver la asignación dentro de una función y llamar a esa función con un IFS modificado. En ese caso, no es necesario guardar / restaurar por separado IFS. Gracias por "Bize" por señalar eso.


!"#$%&/()[]{}*? are no problembueno ... no del todo: []*?son personajes glob. Entonces, ¿qué pasa con la creación de este directorio y archivo: `mkdir '!" # $% &'; Touch '! "# $% & / () [] {} Te tengo jajajaja - ¿no hay problema' y ejecutas tu comando? simple puede ser hermoso, pero cuando está roto, está roto.
gniourf_gniourf

@gniourf_gniourf La cadena se almacena en una variable. Por favor vea la pregunta original.
ajaaskel

1
@ajaaskel no entendiste completamente mi comentario. Ir en un directorio temporal y emitir estos comandos: mkdir '!"#$%&'; touch '!"#$%&/()[]{} got you hahahaha - are no problem'. Debo admitir que solo crearán un directorio y un archivo con nombres de aspecto extraño. A continuación, ejecute los comandos con la exacta INdiste: IN='bla@some.com;john@home.com;Charlie Brown <cbrown@acme.com;!"#$%&/()[]{}*? are no problem;simple is beautiful :-)'. Verá que no obtendrá el resultado que espera. Porque estás usando un método sujeto a expansiones de nombre de ruta para dividir tu cadena.
gniourf_gniourf

Esto es para demostrar que los personajes *, ?, [...]e incluso, si extglobse establece, !(...), @(...), ?(...), +(...) son problemas con este método!
gniourf_gniourf

1
@gniourf_gniourf Gracias por los comentarios detallados sobre globbing. Ajusté el código para eliminarlo. Sin embargo, mi punto era solo mostrar que una asignación bastante simple puede hacer el trabajo de división.
ajaaskel

1

Ok chicos!

Aquí está mi respuesta!

DELIMITER_VAL='='

read -d '' F_ABOUT_DISTRO_R <<"EOF"
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04.4 LTS"
NAME="Ubuntu"
VERSION="14.04.4 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04.4 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
EOF

SPLIT_NOW=$(awk -F$DELIMITER_VAL '{for(i=1;i<=NF;i++){printf "%s\n", $i}}' <<<"${F_ABOUT_DISTRO_R}")
while read -r line; do
   SPLIT+=("$line")
done <<< "$SPLIT_NOW"
for i in "${SPLIT[@]}"; do
    echo "$i"
done

¿Por qué este enfoque es "el mejor" para mí?

Por dos razones:

  1. No necesita escapar del delimitador;
  2. No tendrá problemas con los espacios en blanco . ¡El valor se separará correctamente en la matriz!

[]


FYI, /etc/os-releasey /etc/lsb-releaseestán destinados a ser obtenidos, y no analizados. Entonces tu método está realmente equivocado. Además, no estás respondiendo la pregunta sobre dividir una cadena en un delimitador.
gniourf_gniourf

0

Una línea para dividir una cadena separada por ';' en una matriz es:

IN="bla@some.com;john@home.com"
ADDRS=( $(IFS=";" echo "$IN") )
echo ${ADDRS[0]}
echo ${ADDRS[1]}

Esto solo establece IFS en una subshell, por lo que no tiene que preocuparse por guardar y restaurar su valor.


-1 esto no funciona aquí (ubuntu 12.04). imprime solo el primer eco con todo el valor $ IN, mientras que el segundo está vacío. puedes verlo si pones echo "0:" $ {ADDRS [0]} \ n echo "1:" $ {ADDRS [1]} la salida es 0: bla@some.com;john@home.com\n 1:(\ n es una nueva línea)
Luca Borrione

1
consulte la respuesta de nickjb en para una alternativa de trabajo a esta idea stackoverflow.com/a/6583589/1032370
Luca Borrione

1
-1, 1. IFS no se está configurando en esa subshell (se está pasando al entorno de "echo", que es una función incorporada, por lo que no sucede nada de todos modos). 2. $INse cita para que no esté sujeto a la división IFS. 3. La sustitución del proceso se divide por espacios en blanco, pero esto puede corromper los datos originales.
Score_Under

0

Quizás no sea la solución más elegante, pero funciona con *y espacios:

IN="bla@so me.com;*;john@home.com"
for i in `delims=${IN//[^;]}; seq 1 $((${#delims} + 1))`
do
   echo "> [`echo $IN | cut -d';' -f$i`]"
done

Salidas

> [bla@so me.com]
> [*]
> [john@home.com]

Otro ejemplo (delimitadores al principio y al final):

IN=";bla@so me.com;*;john@home.com;"
> []
> [bla@so me.com]
> [*]
> [john@home.com]
> []

Básicamente, elimina todos los personajes que no sean, ;por delimsejemplo, hacer . ;;;. Luego se forrepite de 1a number-of-delimiterscomo lo cuenta ${#delims}. El paso final es obtener la $iparte th de forma segura cut.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.