¿Cómo puedo dividir una cadena en un delimitador en Bash?

Tengo esta cadena almacenada en una variable:

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

Ahora me gustaría dividir las cuerdas ; delimitador para que yo tenga:

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

No necesariamente necesito las variables ADDR1 y ADDR2 . Si son elementos de una matriz que es aún 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="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] 

Hubo una solución que implicaba establecer Internal_field_separator (IFS) ; . No estoy seguro de lo que sucedió con esa respuesta, ¿cómo restablecer IFS a su valor predeterminado?

Solución RE: IFS , probé esto y funciona, guardo el viejo IFS y 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 lo intenté

 mails2=($IN) 

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

Puede configurar la variable separadora de campo interno (IFS) y dejarla analizar en una matriz. Cuando esto ocurre en un comando, la asignación a IFS solo tiene lugar en el entorno de ese único comando (para read ). Luego analiza la entrada de acuerdo con el valor de la variable IFS 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 entero 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" 

Tomado de Bash Shell script split array :

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

Explicación:

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

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

Hay algunos errores comunes:

  1. Si la cadena original tiene espacios, deberá 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;

Si no te importa procesarlos inmediatamente, me gusta hacer esto:

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

Podría utilizar este tipo de bucle para inicializar una matriz, pero probablemente haya una manera más fácil de hacerlo. Espero que esto ayude, sin embargo.

Respuesta compatible

Para esta pregunta SO, ya hay muchas formas diferentes de hacer esto en bash . Pero bash tiene muchas características especiales , llamadas bashism que funcionan bien, pero eso no funcionará en ningún otro shell .

En particular, las matrices , la matriz asociativa y la sustitución de patrones son bashisms puros y pueden no funcionar bajo otros shells .

En mi Debian GNU / Linux , hay un shell estándar llamado dash , pero conozco mucha gente a la que le gusta usar ksh .

Finalmente, en una situación muy pequeña, hay una herramienta especial llamada busybox con su propio intérprete de shell ( ash ).

Cadena solicitada

La muestra de cadena en la pregunta SO es:

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

Como esto podría ser útil con espacios en blanco y como los espacios en blanco podrían modificar el resultado de la rutina, prefiero usar esta cadena de muestra:

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

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

Bajo pure bash, podemos usar matrices e IFS :

 var="bla@some.com;john@home.com;Full Name " 

 oIFS="$IFS" IFS=";" declare -a fields=($var) IFS="$oIFS" unset oIFS 

 IFS=\; read -a fields < <<"$var" 

El uso de esta syntax en bash recientes no cambia $IFS para la sesión actual, sino solo para el comando actual:

 set | grep ^IFS= IFS=$' \t\n' 

Ahora la cadena var se divide y almacena en una matriz ( fields nombre):

 set | grep ^fields=\\\|^var= fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name ") var='bla@some.com;john@home.com;Full Name ' 

Podríamos solicitar contenido variable con declare -p :

 declare -p var fields declare -- var="bla@some.com;john@home.com;Full Name " declare -a fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name ") 

read es la forma más rápida de hacer la división, porque no hay bifurcaciones ni recursos externos llamados.

A partir de ahí, puede usar la syntax que ya conoce para procesar cada campo:

 for x in "${fields[@]}";do echo "> [$x]" done > [bla@some.com] > [john@home.com] > [Full Name ] 

o soltar cada campo después del procesamiento (me gusta este enfoque cambiante ):

 while [ "$fields" ] ;do echo "> [$fields]" fields=("${fields[@]:1}") done > [bla@some.com] > [john@home.com] > [Full Name ] 

o incluso para impresión simple (syntax más corta):

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

División de cadena basada en delimitador en shell

Pero si escribes algo utilizable debajo de muchos caparazones, no debes usar bashisms .

Hay una syntax, utilizada en muchos shells, para dividir una cadena en la primera o última aparición de una subcadena:

 ${var#*SubStr} # will drop begin of string up to first occur of `SubStr` ${var##*SubStr} # will drop begin of string up to last occur of `SubStr` ${var%SubStr*} # will drop part of string from last occur of `SubStr` to the end ${var%%SubStr*} # will drop part of string from first occur of `SubStr` to the end 

(La falta de esto es la razón principal de mi publicación de respuesta;)

Como se señala por Score_Under :

# y % eliminan la cadena de coincidencia más corta posible, y

## y %% eliminar el más largo posible.

Esta pequeña secuencia de comandos de muestra funciona bien bajo bash , dash , ksh , busybox y también se probó en el bash de Mac-OS:

 var="bla@some.com;john@home.com;Full Name " while [ "$var" ] ;do iter=${var%%;*} echo "> [$iter]" [ "$var" = "$iter" ] && \ var='' || \ var="${var#*;}" done > [bla@some.com] > [john@home.com] > [Full Name ] 

¡Que te diviertas!

¿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

He visto un par de respuestas que hacen referencia al comando de cut , 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 un conjunto de scripts bash, tr es probablemente más eficiente, pero se puede usar el cut , y es más efectivo si desea extraer campos específicos del centro.

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 tienes un archivo de registro delimitado con filas como esta:

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

cut es muy útil para poder utilizar este archivo y seleccionar un campo en particular para su posterior procesamiento.

Esto funcionó para mí:

 string="1;2" echo $string | cut -d';' -f1 # output is 1 echo $string | cut -d';' -f2 # output is 2 
 echo "bla@some.com;john@home.com" | sed -e 's/;/\n/g' bla@some.com john@home.com 

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.

Creo que AWK es el mejor y más eficiente comando para resolver su problema. AWK está incluido en Bash de forma predeterminada 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 mediante la redefinición del campo de impresión awk.

Una visió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) 

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 opción -d de read (delimitador) con un delimitador vacío, por lo que la read se fuerza a leer todo lo que se alimenta. Y alimentamos la read con exactamente el contenido de la variable, sin línea nueva final gracias a printf . Tenga en cuenta que también estamos poniendo el delimitador en printf para garantizar que la cadena que se pasa a read tenga un delimitador final. Sin él, read recortaría los posibles campos finales vacíos:

 $ 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]="")' 

el campo vacío final se conserva.


Actualización para Bash≥4.4

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

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

¿Qué tal este delineador, si no estás usando matrices?

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

Sin configurar el IFS

Si solo tiene un punto, puede hacer eso:

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

conseguirás:

 b = foo c = bar 

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 IFS delimita las palabras basadas en el separador y () se usa para crear una matriz . Entonces [@] 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, sin unset IFS .

Hay una manera simple e inteligente como esta:

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

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

 brew install findutils 

entonces

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

Esta es la forma más simple de hacerlo.

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

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, conectarse 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:

  • No se sustituye a IFS : debido al scope dynamic de incluso las variables locales, la anulación de IFS sobre un bucle hace que el nuevo valor se filtre en llamadas de función realizadas desde dentro del bucle.

  • Las matrices no se utilizan: la lectura de una cadena en una matriz utilizando read requiere la bandera -a en Bash y -A en zsh.

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

 #!/usr/bin/env bash split() { # ... } split "$@" 
 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

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" 

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]} 

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

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

Ahora ${a[0]} , ${a[1]} , etc., son como era de esperar. Use ${#a[*]} para la cantidad de 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 qué preocuparse, lo que solucionó mi problema, pero puede que no resuelva el tuyo. Vaya con la solución $IFS en ese caso.

Use el set incorporado 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 

Dos alternativas bourne-ish donde ninguno requiere arrays bash:

Caso 1 : Que sea sencillo: use una NewLine como el separador de registros … ej.

 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: Tal vez vale la pena usar NL extensamente internamente , y solo convertir a una RS diferente cuando se genera el resultado final externamente .

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

 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 ser compuesta dentro del ciclo es persistente después de que el ciclo se haya completado. Esto es útil cuando se manipulan listas en la memoria, en lugar de almacenar listas en archivos. {ps mantener la calma y continuar B-)}

Además de las fantásticas respuestas que ya se proporcionaron, si solo se trata 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 ; , de modo que pueda recorrer los campos con un bucle for 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;cd;e_;f" > [a] > [b] > [cd] > [e_] > [f] 

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 

Qué es el trabajo es:

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

donde // significa reemplazo global.

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 necesita escapar del delimitador;
  2. No tendrás problemas con espacios en blanco . ¡El valor estará separado correctamente en la matriz!

[] 's

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.

 IN='bla@some.com;john@home.com;Charlie Brown  

Salida:

 bla@some.com john@home.com Charlie Brown  

Explanation: Simple assignment using parenthesis () converts semicolon separated list into an array provided you have correct IFS while doing that. Standard FOR loop handles individual items in that array as usual. Notice that the list given for IN variable must be "hard" quoted, that is, with single ticks.

IFS must be saved and restred since Bash does not treat an assignment the same way as a command. An alternate workaround is to wrap the assignment inside a function and call that function with a modified IFS. In that case separate saving/restring of IFS is not needed. Thanks for "Bize" for pointing that out.

Maybe not the most elegant solution, but works with * and spaces:

 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] 

Other example (delimiters at beginning and end):

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

Basically it removes every character other than ; making delims eg. ;;; . Then it does for loop from 1 to number-of-delimiters as counted by ${#delims} . The final step is to safely get the $i th part using cut .