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


14 Answers

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 sintaxis 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;
Question

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.




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 ninguna nueva línea 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")



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 alcance dinámico 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 "$@"



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



Esto funcionó para mí:

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



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.




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;c   d;e_;f"
> [a]
> [b]
> [c   d]
> [e_]
> [f]



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 <fulnam@other.org>"

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 <fulnam@other.org>"

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

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

El uso de esta sintaxis 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 <fulnam@other.org>")
var='bla@some.com;john@home.com;Full Name <fulnam@other.org>'

Podríamos solicitar contenido variable con declare -p :

declare -p var fields
declare -- var="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>")

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 sintaxis que ya conoce para procesar cada campo:

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

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 <fulnam@other.org>]

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

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

División de cadena basada en delimitador en shell

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

Hay una sintaxis, 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 <fulnam@other.org>"
while [ "$var" ] ;do
    iter=${var%%;*}
    echo "> [$iter]"
    [ "$var" = "$iter" ] && \
        var='' || \
        var="${var#*;}"
  done
> [bla@some.com]
> [john@home.com]
> [Full Name <fulnam@other.org>]

¡Que te diviertas!




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



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




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 .




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



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.




you can apply awk to many situations

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

also you can use this

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



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



Related