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

2369

Tengo esta cadena almacenada en una variable:

IN="[email protected];[email protected]"

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

ADDR1="[email protected]"
ADDR2="[email protected]"

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


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

#!/usr/bin/env bash

IN="[email protected];[email protected]"

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

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

Producción:

> [[email protected]]
> [[email protected]]

Hubo una solución que implicaba configurar Internal_field_separator (IFS) en ;. No estoy seguro de qué sucedió con esa respuesta, ¿cómo se restablece IFSa los valores predeterminados?

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

IN="[email protected];[email protected]"

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

IFS=$OIFS

Por cierto, cuando lo intenté

mails2=($IN)

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

8
  • 21
    Con respecto a su "Edit2": simplemente puede "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 configurado 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 1 de mayo de 2012 a las 1:26
  • 22
    @BrooksMoses: (a) +1 para usar local IFS=...cuando sea posible; (b) -1 porque 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 suponga ciegamente que su código nunca será invocado con IFS configurado en un valor personalizado; (c) otra idea es invocar una subcapa: (IFS=$custom; ...)cuando la subcapa sale, IFS volverá a lo que era originalmente. dubiousjim 31 de mayo de 2012 a las 5:21
  • Solo quiero echar un vistazo rápido a las rutas para decidir dónde lanzar un ejecutable, así que recurrí a ejecutar ruby -e "puts ENV.fetch('PATH').split(':')". Si desea permanecer puro, bash no ayudará, pero usar cualquier lenguaje de scripting que tenga una división incorporada es más fácil. nicooga 7 de marzo de 2016 a las 15:32
  • 7
    for x in $(IFS=';';echo $IN); do echo "> [$x]"; doneuser2037659 26/04/18 a las 20:15
  • 3
    Para guardarlo como una matriz, tuve que colocar otro paréntesis y cambiarlo \npor solo un espacio. Entonces la línea final es mails=($(echo $IN | tr ";" " ")). Entonces ahora puedo verificar los elementos mailsusando la notación de matriz mails[index]o simplemente iterando en un bucleafranques 3 de julio de 2018 a las 14:08
1430

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 se lleva a cabo en el entorno de ese comando (a read). Luego analiza la entrada de acuerdo con el IFSvalor de la variable en una matriz, que luego podemos iterar.

Este ejemplo analizará una línea de elementos separados por ;, insertándola en una matriz:

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

Este otro ejemplo es para procesar todo el contenido de $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"
16
  • 26
    Probablemente esta sea la mejor manera. ¿Cuánto tiempo persistirá IFS en su valor actual? ¿Puede estropear mi código si se configura cuando no debería, y cómo puedo restablecerlo cuando haya terminado? Chris Lutz 28 de mayo de 2009 a las 2:25
  • 7
    ahora después de la corrección aplicada, solo dentro de la duración del comando de lectura :)Johannes Schaub - litb 28 de mayo de 2009 a las 3:04
  • 18
    Puede leer todo a la vez sin usar un bucle while: read -r -d '' -a addr <<< "$ in" # La -d '' es la clave aquí, le dice a read que no se detenga en la primera línea nueva ( que es el valor predeterminado -d) pero continuar hasta EOF o un byte NULL (que solo ocurre en datos binarios). lhunath 28 de mayo de 2009 a las 6:14
  • 66
    @LucaBorrione La configuración IFSen la misma línea que el readsin punto y coma u otro separador, a diferencia de en un comando separado, lo ajusta a ese comando, por lo que siempre está "restaurado"; no necesita hacer nada manualmente. Charles Duffy 6 de julio de 2013 a las 14:39
  • 5
    @imagineerThis Hay un error relacionado con las cadenas de caracteres y los cambios locales en IFS que requieren $INuna cita. El error se corrigió en bash4.3. chepner 2 oct 2014 a las 3:50
1202

Tomado de la matriz dividida del script de shell Bash :

IN="[email protected];[email protected]"
arrIN=(${IN//;/ })
echo ${arrIN[1]}                  # Output: [email protected]

Explicación:

Esta construcción reemplaza todas las apariciones 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, deberá usar IFS :
  • IFS=':'; arrIN=($IN); unset IFS;
  1. Si la cadena original tiene espacios y el delimitador es una nueva línea, puede establecer IFS con:
  • IFS=$'\n'; arrIN=($IN); unset IFS;
20
  • 105
    Solo quiero agregar: este es el más simple de todos, puede acceder a los elementos de la matriz con $ {arrIN [1]} (comenzando desde ceros, por supuesto)oz123 21/03/11 a las 18:50
  • 30
    Lo encontré: la técnica de modificar una variable dentro de un $ {} se conoce como 'expansión de parámetros'. KomodoDave 5 de ene. De 2012 a las 15:13
  • 26
    No, no creo que esto funcione cuando también hay espacios presentes ... está convirtiendo el ',' a '' y luego construyendo una matriz separada por espacios. Ethan 12/04/2013 a las 22:47
  • 15
    Muy conciso, pero hay advertencias para uso general : el shell aplica división de palabras y expansiones a la cadena, lo que puede no ser deseado; solo pruébalo con. IN="[email protected];[email protected];*;broken apart". En resumen: este enfoque se romperá si sus tokens contienen espacios y / o caracteres incrustados. como *eso sucede para hacer que un token coincida con los nombres de archivo en la carpeta actual. mklement0 24/04/13 a las 14:08
  • 62
    Este es un mal enfoque por otras razones: por ejemplo, si su cadena contiene ;*;, *se expandirá a una lista de nombres de archivo en el directorio actual. -1Charles Duffy 6 de julio de 2013 a las 14:39
291

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

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

Puede usar este tipo de bucle para inicializar una matriz, pero probablemente haya una forma más fácil de hacerlo. Espero que esto ayude, sin embargo.

5
  • Deberías haber guardado la respuesta IFS. Me enseñó algo que no sabía, y definitivamente hizo una matriz, mientras que esto solo es un sustituto barato. Chris Lutz 28 de mayo de 2009 a las 2:42
  • Veo. Sí, al hacer estos experimentos tontos, voy a aprender cosas nuevas cada vez que trato de responder. He editado cosas basadas en los comentarios de #bash IRC y las he recuperado :)Johannes Schaub - litb 28 de mayo de 2009 a las 2:59
  • 3
    Puede cambiarlo para que haga eco de "$ IN" | tr ';' '\ n' | mientras lee -r ADDY; hacer # procesar "$ ADDY"; hecho para que tenga suerte, creo :) Tenga en cuenta que esto se bifurcará, y no puede cambiar las variables externas desde dentro del ciclo (es por eso que usé la sintaxis <<< "$ IN") entoncesJohannes Schaub - litb 28 de mayo de 2009 a las 17:00
  • 11
    Para resumir el debate en los comentarios: Advertencias para uso general : el shell aplica división de palabras y expansiones a la cadena, lo que puede no ser deseado; solo pruébalo con. IN="[email protected];[email protected];*;broken apart". En resumen: este enfoque se romperá si sus tokens contienen espacios y / o caracteres incrustados. como *eso sucede para hacer que un token coincida con los nombres de archivo en la carpeta actual. mklement0 24/04/13 a las 14:13
  • Esta es una respuesta muy útil. ej IN=abc;def;123. ¿Cómo podemos imprimir también el número de índice? echo $count $i ?user8864088 10 oct 2018 a las 18:50
262

Respuesta compatible

Hay muchas formas diferentes de hacer esto en .

Sin embargo, es importante tener en cuenta primero 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 de esta publicación, así como otras en el hilo, son bashismos y pueden no funcionar con otras shells que muchas personas usan.

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

Cadena solicitada

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

IN="[email protected];[email protected]"

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

IN="[email protected];[email protected];Full Name <[email protected]>"

Cadena dividida según el 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, indica bashqué carácter (s) debe tratar como un delimitador entre elementos al definir una matriz:

IN="[email protected];[email protected];Full Name <[email protected]>"

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

En las nuevas versiones de bash, anteponiendo un comando con una definición de IFS IFS cambia para ese comando solamente y se 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 denominada fields, dividida en punto y coma:

set | grep ^fields=\\\|^IN=
# fields=([0]="[email protected]" [1]="[email protected]" [2]="Full Name <[email protected]>")
# IN='[email protected];[email protected];Full Name <[email protected]>'

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

declare -p IN fields
# declare -- IN="[email protected];[email protected];Full Name <[email protected]>"
# declare -a fields=([0]="[email protected]" [1]="[email protected]" [2]="Full Name <[email protected]>")

Tenga en cuenta que reades la forma más rápida de realizar la división porque no hay bifurcaciones ni recursos externos llamados.

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

# `"${fields[@]}"` expands to return every element of `fields` array as a separate argument
for x in "${fields[@]}" ;do
    echo "> [$x]"
    done
# > [[email protected]]
# > [[email protected]]
# > [Full Name <[email protected]>]

O puede eliminar cada campo de la matriz después del procesamiento utilizando un enfoque de cambio , que me gusta:

while [ "$fields" ] ;do
    echo "> [$fields]"
    # slice the array 
    fields=("${fields[@]:1}")
    done
# > [[email protected]]
# > [[email protected]]
# > [Full Name <[email protected]>]

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

printf "> [%s]\n" "${fields[@]}"
# > [[email protected]]
# > [[email protected]]
# > [Full Name <[email protected]>]

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, nuevas líneas 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 e implícitamente "recorrer" 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 \0al final de la cadena de formato es inútil si no le importan los campos vacíos al final de la cadena o si no están presentes).

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

# Seq:      0: Sending mail to '[email protected]', done.
# Seq:      1: Sending mail to '[email protected]', done.
# Seq:      2: Sending mail to 'Full Name <[email protected]>', 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 '[email protected]', done.
# Seq:      1: Sending mail to '[email protected]', done.
# Seq:      2: Sending mail to 'Full Name <[email protected]>', done.

Cadena dividida según el delimitador en

Si no puede usar bash, o si desea escribir algo que se pueda usar en muchas shells diferentes, a menudo no puede usar bashisms , y esto incluye las matrices 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 de la primera o última aparición de un patrón. Tenga en cuenta que *es un comodín que representa 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 estoy escribiendo 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

Como lo explica Score_Under :

# and % delete the shortest possible matching substring from the start and end of the string respectively, and

## and %% delete the longest possible matching substring.

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

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

(Gracias al comentario de Adam Katz, ¡ simplificando mucho este bucle!)

IN="[email protected];[email protected];Full Name <[email protected]>"
while [ "$IN" != "$iter" ] ;do
    # extract the substring from start of string up to delimiter.
    iter=${IN%%;*}
    # delete this first "element" AND next separator, from $IN.
    IN="${IN#$iter;}"
    # Print (or doing anything with) the first "element".
    echo "> [$iter]"
done
# > [[email protected]]
# > [[email protected]]
# > [Full Name <[email protected]>]

¡Divertirse!

7
  • 20
    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 28/04/15 a las 16:58
  • 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 al final. ImHere 26 de octubre de 2016 a las 4:36
  • 10
    Esta respuesta es bastante épica. tylerl 4 sep.20 a las 18:41
  • 1
    Si cambia la whilecondición de la respuesta del shell portátil a [ "$IN" != "$iter" ], no necesitará el condicional al final, solo su cláusula else. Todo el bucle podría condensarse en dos líneas internas:while [ "$IN" != "$iter" ]; do iter="${IN%%;*}" IN="${IN#*;}"; echo "> [$iter]"; doneAdam Katz 2 de julio a las 23:23
  • 1
    @AdamKatz Muy inteligente, respuesta editada, ¡gracias! F. Hauri 3 de julio a las 7:54
247

He visto un par de respuestas que hacen referencia al cutcomando, pero todas han sido eliminadas. Es un poco extraño que nadie haya profundizado en 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 "[email protected];[email protected]" | cut -d ";" -f 1
[email protected]
$ echo "[email protected];[email protected]" | cut -d ";" -f 2
[email protected]

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.

5
  • 11
    Felicitaciones por usar cut, ¡es la herramienta adecuada para el trabajo! Mucho más despejado que cualquiera de esos trucos de shell. MisterMiyagi 2 de nov. De 2016 a las 8:42
  • 6
    Este enfoque solo funcionará si conoce la cantidad 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 14 de septiembre de 2017 a las 8:30
  • 1
    Excamente lo que estaba buscando tratando de evitar una cadena vacía en un csv. Ahora también puedo señalar el valor exacto de la 'columna'. Trabaja con IFS ya utilizado en un bucle. Mejor de lo esperado para mi situación. Louis Loudog Trottier 10 de mayo de 2018 a las 4:20
  • 1
    Muy útil para extraer ID y PID también, es decirMilos Grujic 21/10/19 a las 9:07
  • 3
    Vale la pena desplazarse por esta respuesta más de media página :)Gucu112 3 de ene. De 2020 a las 17:26
157

Esto funcionó para mí:

string="1;2"
echo $string | cut -d';' -f1 # output is 1
echo $string | cut -d';' -f2 # output is 2
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 12/12/18 a las 1:37
  • Respondido hace unos cuatro años por @Ashok , y también, hace más de un año por @DougW , que tu respuesta, con aún más información. Publique una solución diferente a la de los demás. MAChitgarha 3 abr.20 a las 9:41
121

Creo que AWK es el comando mejor y más eficiente para resolver su problema. AWK se incluye de forma predeterminada en casi todas las distribuciones de Linux.

echo "[email protected];[email protected]" | awk -F';' '{print $1,$2}'

daré

[email protected] [email protected]

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

4
  • 8
    O incluso más simple: echo "[email protected]; [email protected]" | awk 'BEGIN {RS = ";"} {print}'Jaro 7 ene 14 a las 21:30
  • @Jaro Esto funcionó perfectamente para mí cuando tenía una cadena con comas y necesitaba reformatearla en líneas. Gracias. Aquarelle 6 de mayo de 2014 a las 21:58
  • 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 forma de configurar una matriz ... ¡mejor! ¡Gracias! Eduardo Lucio 5 de agosto de 2015 a las 12:59
  • @EduardoLucio, lo que estoy pensando es que tal vez primero puedas reemplazar tu delimitador inode=en, ;por ejemplo sed -i 's/inode\=/\;/g' your_file_to_process, por , luego definir -F';'cuándo aplicar awk, espero que eso pueda ayudarte. Tong 6 de agosto de 2015 a las 2:42
96

¿Qué tal este enfoque?

IN="[email protected];[email protected]" 
set -- "$IN" 
IFS=";"; declare -a Array=($*) 
echo "${Array[@]}" 
echo "${Array[0]}" 
echo "${Array[1]}" 

Fuente

6
  • 7
    +1 ... pero no nombraría la variable "Array" ... pet peev, supongo. Buena solución. Yzmir Ramirez 5 de septiembre de 2011 a las 1:06
  • 14
    +1 ... pero el "set" y declare -a son innecesarios. También podrías haber usado soloIFS";" && Array=($IN) ... ata 3/11/11 a las 22:33
  • +1 Solo una nota al margen: ¿no sería recomendable conservar el antiguo 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 estoLuca Borrione 3 de septiembre de 2012 a las 9:26
  • 6
    -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 6 de julio de 2013 a las 14:44
  • 1
    Es mejor utilizar $'...': IN=$'[email protected];[email protected];bet <[email protected]\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 glob, la solución debe incluir set -f. John_West 8 de enero de 2016 a las 12:29
79
echo "[email protected];[email protected]" | sed -e 's/;/\n/g'
[email protected]
[email protected]
4
  • 4
    -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 de septiembre de 2012 a las 10:08
  • 5
    @Luca No, el script sed crea exactamente dos líneas. Lo que crea las múltiples entradas para usted es cuando las coloca en una matriz bash (que se divide en espacios en blanco de forma predeterminada)lothar 3 de septiembre de 2012 a las 17:33
  • Ese es exactamente el punto: el OP necesita almacenar entradas en una matriz para recorrerla, como puede ver en sus ediciones. Creo que su (buena) respuesta no se mencionó para usar arrIN=( $( echo "$IN" | sed -e 's/;/\n/g' ) )para lograr eso, y el consejo de cambiar IFS IFS=$'\n'para aquellos que aterrizan aquí en el futuro y necesitan dividir una cadena que contenga espacios. (y restaurarlo después). :)Luca Borrione 4 de septiembre de 2012 a las 7:09
  • 3
    @Luca Buen punto. Sin embargo, la asignación de la matriz no estaba en la pregunta inicial cuando escribí esa respuesta. lothar 4 de septiembre de 2012 a las 16:55
70

Esto también funciona:

IN="[email protected];[email protected]"
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 solo pase "[email protected]", lo asignará tanto a ADD1 como a ADD2.

1
  • 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 3 de marzo de 2016 a las 17:17
38

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

IN="[email protected];[email protected]"
read ADDR1 ADDR2 <<<$(IFS=";"; echo $IN)
5
  • ¡Creo que sí! Ejecute los comandos anteriores y luego "echo $ ADDR1 ... $ ADDR2" y obtengo la salida "[email protected] ... [email protected]"nickjb 6/10/11 a las 15:33
  • 1
    Esto funcionó REALMENTE bien para mí ... Lo usé para iterar sobre una matriz de cadenas que contenían datos DB, SERVER, PORT separados por comas para usar mysqldump. Nick 28/10/11 a las 14:36
  • 5
    Diagnóstico: la IFS=";"asignación existe solo en la $(...; echo $IN)subcapa; es por eso que algunos lectores (incluyéndome a mí) inicialmente piensan que no funcionará. Asumí que todo $ IN estaba siendo absorbido por ADDR1. Pero nickjb tiene razón; 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. Entonces, el efecto neto es como si uno hubiera llamado read ADDR1 ADDR2 <<< "[email protected] [email protected]"(tenga en cuenta que la entrada está separada por espacios, no; separada). dubiousjim 31 de mayo de 2012 a las 5:28
  • 1
    Esto falla en espacios y nuevas líneas, y también expande comodines *en la echo $INexpansión con una variable sin comillas. ImHere 26 de octubre de 2016 a las 4:43
  • Me gusta mucho esta solución. Una descripción de por qué funciona sería muy útil y lo convertiría en una mejor respuesta general. Michael Gaskill 30 de enero de 2017 a las 2:28
37

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

Mirar:

$ 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 alimentamos readexactamente con el contenido de la variable in, sin saltos de línea finales gracias a printf. Tenga en cuenta que también estamos colocando el delimitador printfpara asegurarnos de que la cadena pasada a readtenga un delimitador final. Sin él, readrecortaría 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 builtin 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")
2
  • 5
    Lo encontré como la solución rara en esa lista que funciona correctamente con \nespacios y *simultáneamente. Además, no hay bucles; Se puede acceder a la variable de matriz 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 8 de enero de 2016 a las 12:10
  • El mapfileejemplo falla si quiero usarlo %como delimitador. Sugiero printf '%s' "$in%". Robin A. Meade 10 de julio a las 3:08
36

¿Qué tal este trazador de líneas, si no está utilizando matrices?

IFS=';' read ADDR1 ADDR2 <<<$IN
5
  • 1
    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 un solo carácter de tabulación). dubiousjim 31 de mayo de 2012 a las 5:36
  • -1 Esto no funciona aquí (ubuntu 12.04). Si se agrega echo "ADDR1 $ADDR1"\n echo "ADDR2 $ADDR2"a su fragmento, se generará ADDR1 [email protected] [email protected]\nADDR2(\ n es una nueva línea)Luca Borrione 3 de septiembre de 2012 a las 10:07
  • 1
    Esto probablemente se deba a un error que involucra IFSy aquí cadenas que se corrigieron en bash4.3. La cotización $INdebería solucionarlo. (En teoría, $INno está sujeto a la división de palabras o al agrupamiento después de que se expande, lo que significa que las comillas deberían ser innecesarias. Sin embargo, incluso en 4.3, hay al menos un error restante, informado y programado para ser corregido, por lo que las citas siguen siendo una buena opción. idea.)chepner 19 de septiembre de 2015 a las 13:59
  • Esto se rompe si $ in contiene nuevas líneas incluso si se cotiza $ IN. Y agrega una nueva línea al final. ImHere 26 de octubre de 2016 a las 4:55
  • Un problema con esto, y muchas otras soluciones, es también que asume que hay EXACTAMENTE DOS elementos en $ IN - O que está dispuesto a que el segundo y los siguientes elementos se rompan en ADDR2. Entiendo que esto cumple con la pregunta, pero es una bomba de tiempo. Steven the Easily Amused 1 de septiembre de 2019 a las 14:36
30

Sin configurar el IFS

Si solo tiene dos puntos, puede hacer eso:

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

conseguirás:

b = foo
c = bar
22

Aquí hay un 3-liner limpio:

in="[email protected];[email protected];[email protected];[email protected]"
IFS=';' list=($in)
for item in "${list[@]}"; do echo $item; done

donde IFSdelimita palabras según el separador y ()se usa para crear una matriz . Luego [@]se usa para devolver cada artículo como una palabra separada.

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

1
  • 5
    El uso de $incomillas sin comillas permite expandir los comodines. ImHere 26/10/2016 a las 5:03
13

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, enviarse 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 IFSsobre un bucle hace que el nuevo valor se filtre en las llamadas de función realizadas desde dentro del bucle.

  • Las matrices no se utilizan: leer una cadena en una matriz utilizando 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 "[email protected]"
2
  • No parece funcionar con delimitadores de más de 1 carácter: split = $ (split "$ content" "file: //")madprops 14/06/19 a las 5:23
  • Verdadero - de help read:-d delim continue until the first character of DELIM is read, rather than newlineHalle Knast 14/06/19 a las 18:52
10

puedes aplicar awk a muchas situaciones

echo "[email protected];[email protected]"|awk -F';' '{printf "%s\n%s\n", $1, $2}'

también puedes usar esto

echo "[email protected];[email protected]"|awk -F';' '{print $1,$2}' OFS="\n"
0
8

Hay una forma simple e inteligente como esta:

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

Pero debe usar gnu xargs, BSD xargs no admite -d delim. Si usa Apple Mac como yo. Puede instalar gnu xargs:

brew install findutils

luego

echo "add:sfff" | gxargs -d: -i  echo {}
0
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[*]}
0
4

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

IN="[email protected];[email protected]"
arr=(`echo $IN | tr ';' ' '`)

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

Hay algunas respuestas interesantes aquí (errator especialmente), pero para que algo análogo se divida en otros idiomas, que es lo que entendí que significaba la pregunta original, me decidí por esto:

IN="[email protected];[email protected]"
declare -a a="(${IN/;/ })";

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

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

NOTA IMPORTANTE:

Esto funciona en los casos en los que 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.

2
  • No funciona cuando INcontiene más de dos direcciones de correo electrónico. Consulte la misma idea (pero fija) en la respuesta de palindromoHo 7/10/2013 a las 13:33
  • Mejor uso ${IN//;/ }(doble barra) para que también funcione con más de dos valores. Tenga en cuenta que *?[se expandirá cualquier comodín ( ). Y se descartará un campo vacío al final. ImHere 26/10/2016 a las 5:14
3
IN="[email protected];[email protected]"
IFS=';'
read -a IN_arr <<< "${IN}"
for entry in "${IN_arr[@]}"
do
    echo $entry
done

Producción

[email protected]
[email protected]

Sistema: Ubuntu 12.04.1

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 lo hay. codeforester 2 de enero de 2017 a las 5:37
2

Utilice el setintegrado para cargar la [email protected]matriz:

IN="[email protected];[email protected]"
IFS=';'; set $IN; IFS=$' \t\n'

Entonces, que comience la fiesta:

echo $#
for a; do echo $a; done
ADDR1=$1 ADDR2=$2
1
  • Es mejor usarlo set -- $INpara evitar algunos problemas con "$ IN" que comienzan con un guión. Aún así, la expansión sin comillas de $INexpandirá los comodines ( *?[). ImHere 26/10/2016 a las 5:17
2

Dos alternativas bourne-ish donde ninguna requiere matrices bash:

Caso 1 : Manténgalo agradable y simple: use una nueva línea como separador de registros ... ej.

IN="[email protected]
[email protected]"

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: Tal vez valga la pena usar NL de manera extensiva internamente y solo convertir a un RS diferente cuando se genere el resultado final externamente .

Caso 2 : Uso de ";" como un separador de registros ... ej.

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

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

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

IN="[email protected];[email protected]"
IN="$(conv_IRS ";" <<< "$IN")"

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

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

2

Aparte de las fantásticas respuestas que ya se proporcionaron, si solo es cuestión de imprimir los datos, 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="[email protected];[email protected]"
$ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN"
> [[email protected]]
> [[email protected]]

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
  • 1
    Falla si alguna parte de $ PATH contiene espacios (o líneas nuevas). También expande los comodines (asterisco *, signo de interrogación? Y llaves […]). ImHere 26/10/2016 a las 5:08
2
IN='[email protected];[email protected];Charlie Brown <[email protected];!"#$%&/()[]{}*? 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

Producción:

[email protected]
[email protected]
Charlie Brown <[email protected]
!"#$%&/()[]{}*? 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 el IFS correcto mientras lo hace. El bucle FOR estándar maneja elementos individuales en esa matriz como de costumbre. Observe que la lista proporcionada para la variable IN debe estar entre comillas "estrictas", es decir, con tics únicos.

IFS debe guardarse y restaurarse ya que Bash no trata una asignación 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 IFS por separado. Gracias por "Bize" por señalar eso.

6
  • !"#$%&/()[]{}*? are no problembueno ... no del todo: []*?son personajes glob. Entonces, ¿qué hay de la creación de este directorio y archivo: 'mkdir'! "# $% & '; Touch'!" # $% & / () [] {} Te tengo jajajaja - no hay problema 'y ejecutas tu comando? lo simple puede ser hermoso, pero cuando está roto, está roto. gniourf_gniourf 20/02/15 a las 16:45
  • @gniourf_gniourf La cadena se almacena en una variable. Consulte la pregunta original. ajaaskel 25 feb.15 a las 7:20
  • 1
    @ajaaskel, no entendiste completamente mi comentario. Ir en un directorio temporal y emitir estos comandos: mkdir '!"#$%&'; touch '!"#$%&/()[]{} got you hahahaha - are no problem'. Solo crearán un directorio y un archivo, con nombres extraños, debo admitir. A continuación, ejecute los comandos con la exacta INdiste: IN='[email protected];[email protected];Charlie Brown <[email protected];!"#$%&/()[]{}*? are no problem;simple is beautiful :-)'. Verá que no obtendrá el resultado que espera. Porque está utilizando un método sujeto a expansiones de nombre de ruta para dividir su cadena. gniourf_gniourf 25/02/15 a las 7:26
  • Esto es para demostrar que los personajes *, ?, [...]e incluso, si extglobse establece, !(...), @(...), ?(...), +(...) son problemas con este método! gniourf_gniourf 25/02/15 a las 7:29
  • 1
    @gniourf_gniourf Gracias por los comentarios detallados sobre el globbing. Ajusté el código para que se apagara. Sin embargo, mi punto era solo mostrar que una asignación bastante simple puede hacer el trabajo de división. ajaaskel 26/02/15 a las 15:26
2

¡Bien 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 es necesario escapar del delimitador;
  2. No tendrás problema con los espacios en blanco . ¡El valor se separará correctamente en la matriz!

[]'s

1
  • FYI, /etc/os-releasey /etc/lsb-releaseestán destinados a ser obtenidos, no analizados. Entonces tu método es realmente incorrecto. Además, no está respondiendo la pregunta sobre dividir una cadena en un delimitador. gniourf_gniourf 30 de enero de 2017 a las 8:26
1

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

IN="[email protected];[email protected]"
ADDRS=( $(IFS=";" echo "$IN") )
echo ${ADDRS[0]}
echo ${ADDRS[1]}

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

3
  • -1 esto no funciona aquí (ubuntu 12.04). imprime solo el primer eco con todo el valor $ IN en él, mientras que el segundo está vacío. puedes verlo si pones echo "0:" $ {ADDRS [0]} \ n echo "1:" $ {ADDRS [1]} la salida es 0: [email protected];[email protected]\n 1:(\ n es una nueva línea)Luca Borrione 3 de septiembre de 2012 a las 10:04
  • 1
    consulte la respuesta de nickjb en para obtener una alternativa funcional a esta idea stackoverflow.com/a/6583589/1032370Luca Borrione 3 de septiembre de 2012 a las 10:05
  • 1
    -1, 1. IFS no se está configurando en esa subcapa (se está pasando al entorno de "echo", que es incorporado, por lo que no sucede nada de todos modos). 2. $INse cotiza por lo que no está sujeto a la división de IFS. 3. La sustitución del proceso se divide por espacios en blanco, pero esto puede dañar los datos originales. Score_Under 28/04/15 a las 17:09
0

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

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

Salidas

> [[email protected] me.com]
> [*]
> [[email protected]]

Otro ejemplo (delimitadores al principio y al final):

IN=";[email protected] me.com;*;[email protected];"
> []
> [[email protected] me.com]
> [*]
> [[email protected]]
> []

Básicamente, elimina todos los caracteres que no sean, ;por delimsejemplo, hacer . ;;;. Luego, se forrepite de 1a number-of-delimiterscontado por ${#delims}. El último paso es $iutilizar de forma segura la parte cut.