Dividir cadena en una matriz en Bash


En un script Bash me gustaría dividir una línea en piezas y almacenarlas en una matriz.

La línea:

Paris, France, Europe

Me gustaría tenerlos en una matriz como esta:

array[0] = Paris
array[1] = France
array[2] = Europe

Me gustaría usar código simple, la velocidad del comando no importa. ¿Cómo puedo hacerlo?

Author: codeforester, 2012-05-14

15 answers

IFS=', ' read -r -a array <<< "$string"

Tenga en cuenta que los caracteres en $IFSse tratan individualmente como separadores de modo que en este caso los campos pueden ser separados por ya sea una coma o un espacio en lugar de la secuencia de los dos caracteres. Curiosamente, sin embargo, los campos vacíos no se crean cuando aparece un espacio de coma en la entrada porque el espacio se trata especialmente.

Para acceder a un elemento individual:

echo "${array[0]}"

Para iterar sobre los elementos:

for element in "${array[@]}"
do
    echo "$element"
done

Para obtener tanto el índice como el valor:

for index in "${!array[@]}"
do
    echo "$index ${array[index]}"
done

El último ejemplo es útil porque los arrays Bash son escasos. En otras palabras, puede eliminar un elemento o agregar un elemento y luego los índices no son contiguos.

unset "array[1]"
array[42]=Earth

Para obtener el número de elementos en un array:

echo "${#array[@]}"

Como se mencionó anteriormente, los arrays pueden ser dispersos, por lo que no debe usar la longitud para obtener el último elemento. Así es como puedes hacerlo en Bash 4.2 y posteriores:

echo "${array[-1]}"

En cualquier versión de Bash (de algún lugar después 2.05 b):

echo "${array[@]: -1:1}"

Los desplazamientos negativos más grandes seleccionan más lejos del final de la matriz. Anote el espacio antes del signo menos en el formulario anterior. Es necesario.

 808
Author: Dennis Williamson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-27 15:02:34

Aquí hay una manera sin establecer IFS:

string="1:2:3:4:5"
set -f                      # avoid globbing (expansion of *).
array=(${string//:/ })
for i in "${!array[@]}"
do
    echo "$i=>${array[i]}"
done

La idea es usar el reemplazo de cadenas:

${string//substring/replacement}

Para reemplazar todas las coincidencias de subst substring con espacios en blanco y luego usar la cadena sustituida para inicializar una matriz:

(element1 element2 ... elementN)

Nota: esta respuesta hace uso del operador split+glob. Por lo tanto, para evitar la expansión de algunos caracteres (como *), es una buena idea pausar el globbing para este script.

 193
Author: Jim Ho,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-13 12:36:27

Todas las respuestas a esta pregunta son erróneas de una manera u otra.


respuesta Incorrecta #1

IFS=', ' read -r -a array <<< "$string"

1: Esto es un mal uso de $IFS. El valor de la variable $IFS es no tomado como un separador de cadena de longitud variable única, sino que se toma como un conjunto de separadores de cadena de carácter único, donde cada campo que read se separa de la entrada la línea puede ser terminada por cualquier carácter en el conjunto (coma o espacio, en este ejemplo).

En realidad, para los verdaderos adherentes, el significado completo de $IFS está un poco más involucrado. Del manual de bash :

El shell trata cada carácter de IFS como un delimitador, y divide los resultados de las otras expansiones en palabras usando estos caracteres como terminadores de campo. Si IFS no está configurado, o su el valor es exactamente , el valor predeterminado, luego las secuencias de , , y al principio y al final de los resultados de las expansiones anteriores se ignoran, y cualquier secuencia de caracteres IFS que no estén al principio o al final sirve para delimitar palabras. Si IFS tiene un valor distinto al predeterminado, entonces las secuencias de los caracteres de espacio en blanco , , y se ignoran al principio y al final de la palabra, siempre que el carácter de espacio en blanco esté en el valor de IFS (un carácter de espacio en blanco IFS). Cualquier carácter en IFS que no sea IFS espacio en blanco, junto con cualquier carácter adyacente IFS espacio en blanco, delimita un campo. Una secuencia de caracteres de espacio en blanco IFS también se trata como delimitador. Si el valor de IFS es nulo, no hay división de palabras ocurrir.

Básicamente, para valores no nulos no predeterminados de $IFS, los campos se pueden separar con (1) una secuencia de uno o más caracteres que son todos del conjunto de "IFS caracteres de espacio en blanco" (es decir, cualquiera de , , y ("newline" que significa line feed (LF)) están presentes en cualquier lugar de $IFS), o (2) cualquier carácter que no sea"IFS espacio en blanco" que esté presente en $IFS junto con caracteres " lo rodean en la línea de entrada.

Para el OP, es posible que el segundo modo de separación que describí en el párrafo anterior sea exactamente lo que quiere para su cadena de entrada, pero podemos estar bastante seguros de que el primer modo de separación que describí no es correcto en absoluto. Por ejemplo, ¿qué pasaría si su cadena de entrada fuera 'Los Angeles, United States, North America'?

IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -p a;
## declare -a a=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America")

2: Incluso si utilizara esta solución con un separador de un solo carácter (como una coma por sí misma, es decir, sin espacio posterior u otro equipaje), si el valor de la variable $string contiene cualquier LFs, entonces read dejará de procesarse una vez que encuentre el primer LF. El builtin read solo procesa una línea por invocación. Esto es cierto incluso si está canalizando o redirigiendo input solo a la instrucción read, como estamos haciendo en este ejemplo con el mecanismo here-string , y por lo tanto se garantiza que la entrada no procesada se perderá. El código que alimenta el read builtin no tiene conocimiento del flujo de datos dentro de su estructura de comandos contenedora.

Se podría argumentar que es poco probable que esto cause un problema, pero aún así, es un riesgo sutil que debe evitarse si es posible. Es causado por el hecho de que el builtin read realmente hace dos niveles de división de entrada: primero en líneas, luego en campos. Dado que el OP solo quiere un nivel de división, este uso del builtin read no es apropiado, y debemos evitar se.

3: Un problema potencial no obvio con esta solución es que read siempre elimina el campo final si está vacío, aunque conserva los campos vacíos de lo contrario. Aquí hay una demostración:

string=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -p a;
## declare -a a=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="")

Tal vez a la OP no le importaría esto, pero sigue siendo una limitación que vale la pena conocer. Reduce la robustez y generalidad de la solución.

Este problema se puede resolver añadiendo un delimitador final ficticio a la cadena de entrada justo antes de alimentándolo a read, como demostraré más adelante.


respuesta Incorrecta #2

string="1:2:3:4:5"
set -f                     # avoid globbing (expansion of *).
array=(${string//:/ })

Idea similar:

t="one,two,three"
a=($(echo $t | tr ',' "\n"))

(Nota: He añadido los paréntesis que faltan alrededor de la sustitución de comandos que el respondedor parece haber omitido.)

Idea similar:

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)

Estas soluciones aprovechan la división de palabras en una asignación de matriz para dividir la cadena en campos. Curiosamente, al igual que read, la división general de palabras también utiliza la variable especial $IFS, aunque en este caso se da a entender que se establece en su valor predeterminado de , y por lo tanto cualquier secuencia de uno o más caracteres IFS (que ahora son todos caracteres de espacio en blanco) se considera un delimitador de campos.

Esto resuelve el problema de dos niveles de división cometidos por read, ya que la división de palabras por sí misma constituye solo un nivel de división. Pero sólo como antes, el problema aquí es que los campos individuales en la cadena de entrada ya pueden contener caracteres $IFS, y por lo tanto se dividirían incorrectamente durante la operación de división de palabras. Este no es el caso de ninguna de las cadenas de entrada de muestra proporcionadas por estos respondedores (qué conveniente...), pero por supuesto eso no cambia el hecho de que cualquier código base que use este modismo correría el riesgo de explotar si esta suposición alguna vez se violara en algún momento de la línea. Una vez más, considere mi contraejemplo de 'Los Angeles, United States, North America' (o 'Los Angeles:United States:North America').

Además, la división de palabras normalmente es seguida por expansión de nombre de archivo (aka pathname expansion aka globbing), que, si se hace, podría corromper las palabras que contienen los caracteres *, ?, o [ seguido de ] (y, si extglob se establece, fragmentos entre paréntesis precedidos por ?, *, +, @, o !) comparándolos con objetos del sistema de archivos y expandiendo las palabras ("globs") en consecuencia. El primero de estos tres respondedores ha socavado hábilmente este problema ejecutando set -f de antemano para desactivar el globbing. Técnicamente esto funciona (aunque probablemente debería agregar set +f después para volver a habilitar el globbing para el código posterior que puede depender de él), pero no es deseable tener que meterse con la configuración global del shell para hackear una operación básica de análisis de cadena a matriz en código local.

Otro problema con esta respuesta es que todos los campos vacíos se perderán. Esto puede ser o no un problema, dependiendo de la aplicación.

Nota: Si va a usar esta solución, es mejor usar la forma ${string//:/ } "sustitución de patrones" de expansión de parámetros, en lugar de tomarse la molestia de invocar una sustitución de comandos (que bifurca el shell), iniciar una canalización y ejecutar un ejecutable externo (tr o sed), ya que la expansión de parámetros es puramente una operación interna del shell. (También, para el tr y sed soluciones, la variable de entrada debe estar entre comillas dobles dentro de la sustitución de comandos; de lo contrario, la división de palabras tendría efecto en el comando echo y podría interferir con los valores de campo. Además, la forma $(...) de sustitución de órdenes es preferible a la antigua forma `...` ya que simplifica el anidamiento de las sustituciones de órdenes y permite un mejor resaltado de sintaxis por parte de los editores de texto.)


respuesta Incorrecta #3

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

Esta respuesta es casi la misma que #2. La diferencia es que el respondedor ha hecho la suposición de que los campos están delimitados por dos caracteres, uno de los cuales está representado en el predeterminado $IFS, y el otro no. Ha resuelto este caso bastante específico eliminando el carácter no representado por IFS utilizando una expansión de sustitución de patrones y luego utilizando la división de palabras para dividir los campos en el delimitador representado por IFS que sobrevive caracter.

Esta no es una solución muy genérica. Además, se puede argumentar que la coma es realmente el carácter delimitador "primario" aquí, y que despojarlo y luego depender del carácter de espacio para dividir campos es simplemente incorrecto. Una vez más, considere mi contraejemplo: 'Los Angeles, United States, North America'.

También, de nuevo, la expansión del nombre de archivo podría corromper las palabras expandidas, pero esto se puede evitar deshabilitando temporalmente el globbing para la asignación con set -f y luego set +f.

También, de nuevo, todos los campos vacíos se perderán, lo que puede o no ser un problema dependiendo de la aplicación.


respuesta Incorrecta #4

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

Esto es similar a #2 y #3 ya que utiliza la división de palabras para realizar el trabajo, solo que ahora el código establece explícitamente $IFS para contener solo el delimitador de campo de un solo carácter presente en la cadena de entrada. Debe repetirse que esto no puede funcionar con delimitadores de campos de múltiples caracteres como el delimitador de espacio de coma del OP. Pero para un delimitador de un solo carácter como el LF utilizado en este ejemplo, en realidad se acerca a ser perfecto. Los campos no se pueden dividir involuntariamente en el medio como vimos con las respuestas incorrectas anteriores, y solo hay un nivel de división, según sea necesario.

Un problema es que la expansión del nombre de archivo corromperá las palabras afectadas como se describió anteriormente, aunque una vez más esto se puede resolver mediante envolviendo la declaración crítica en set -f y set +f.

Otro problema potencial es que, dado que LF califica como un "carácter de espacio en blanco IFS" como se definió anteriormente, todos los campos vacíos se perderán, al igual que en #2 y #3. Esto, por supuesto, no sería un problema si el delimitador pasa a ser un carácter no"IFS espacio en blanco", y dependiendo de la aplicación puede no importar de todos modos, pero vicia la generalidad de la solución.

So, to sum arriba, suponiendo que tenga un delimitador de un carácter, y sea un carácter que no sea"IFS espacio en blanco" o no le importen los campos vacíos, y envuelva la instrucción crítica en set -f y set +f, entonces esta solución funciona, pero de lo contrario no.

(También, para información, asignar un LF a una variable en bash se puede hacer más fácilmente con la sintaxis $'...', por ejemplo IFS=$'\n';.)


respuesta Incorrecta #5

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

Idea similar:

IFS=', ' eval 'array=($string)'

Esta solución es efectivamente un cruce entre #1 (en que establece $IFS a coma-espacio) y#2-4 (en que utiliza la división de palabras para dividir la cadena en campos). Debido a esto, sufre de la mayoría de los problemas que afligen a todas las respuestas erróneas anteriores, algo así como el peor de todos los mundos.

También, con respecto a la segunda variante, puede parecer que el eval call es completamente innecesario, ya que su argumento es un literal de cadena entre comillas simples, y por lo tanto es conocido estáticamente. Pero en realidad hay un beneficio muy no obvio de usar eval de esta manera. Normalmente, cuando ejecuta un comando simple que consiste en una asignación variable solo , es decir, sin una palabra de comando real que la siga, la asignación tiene efecto en el entorno de shell:

IFS=', '; ## changes $IFS in the shell environment

Esto es cierto incluso si el comando simple implica múltiples asignaciones de variables; de nuevo, mientras no haya una palabra de comando, todas las asignaciones de variables afectan el entorno de shell:

IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment

Pero, si la asignación de variable está adjunta a un nombre de comando (me gusta llamar a esto una "asignación de prefijo"), entonces no afecta al entorno de shell, y en su lugar solo afecta al entorno del comando ejecutado, independientemente de si es un builtin o externo:

IFS=', ' :; ## : is a builtin command, the $IFS assignment does not outlive it
IFS=', ' env; ## env is an external command, the $IFS assignment does not outlive it

Cita pertinente de la manual de bash :

Si no se obtiene ningún nombre de comando, las asignaciones de variables afectan al entorno de shell actual. De lo contrario, las variables se añaden al entorno del comando ejecutado y no afectan al entorno actual del shell.

Es posible explotar esta característica de asignación de variables para cambiar $IFS solo temporalmente, lo que nos permite evitar todo el gambito de guardar y restaurar como el que se está haciendo con el $OIFS variable en la primera variante. Pero el desafío que enfrentamos aquí es que el comando que necesitamos ejecutar es en sí mismo una mera asignación variable, y por lo tanto no implicaría una palabra de comando para hacer que la asignación $IFS sea temporal. Usted podría pensar a sí mismo, bueno, ¿por qué no simplemente añadir una palabra de comando no-op a la declaración como el : builtin ¿para hacer que la asignación $IFS sea temporal? Esto no funciona porque entonces haría que la $array asignación temporal también:

IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command

So, estamos efectivamente en un callejón sin salida, un poco de un catch-22. Pero, cuando eval ejecuta su código, lo ejecuta en el entorno shell, como si fuera un código fuente normal y estático, y por lo tanto podemos ejecutar la asignación $array dentro del argumento eval para que tenga efecto en el entorno shell, mientras que la asignación de prefijo $IFS que tiene el prefijo del comando eval no sobrevivirá al comando eval. Este es exactamente el truco que se está utilizando en la segunda variante de este solución:

IFS=', ' eval 'array=($string)'; ## $IFS does not outlive the eval command, but $array does

Así que, como puede ver, en realidad es un truco bastante inteligente, y logra exactamente lo que se requiere (al menos con respecto a la afectación de asignación) de una manera bastante no obvia. En realidad no estoy en contra de este truco en general, a pesar de la participación de eval; solo tenga cuidado de comillas simples de la cadena de argumentos para protegerse contra las amenazas de seguridad.

Pero de nuevo, debido a la aglomeración de problemas del "peor de todos los mundos", esta sigue siendo una respuesta incorrecta a los requerimientos de la operación.


respuesta Incorrecta #6

IFS=', '; array=(Paris, France, Europe)

IFS=' ';declare -a array=(Paris France Europe)

Um... ¿Qué? El OP tiene una variable string que necesita ser analizada en una matriz. Esta "respuesta" comienza con el contenido literal de la cadena de entrada pegada en un literal de matriz. Supongo que es una forma de hacerlo.

Parece que el respondedor puede haber asumido que la variable $IFS afecta a todos los análisis bash en todos los contextos, lo cual no es cierto. De la fiesta manual:

IFS El Separador de campos interno que se utiliza para dividir palabras después de la expansión y para dividir líneas en palabras con el comando incorporado read. El valor predeterminado es .

Así que la variable especial $IFS solo se usa en dos contextos: (1) división de palabras que se realiza después de la expansión (lo que significa no cuando se analiza el código fuente de bash) y (2) para dividir las líneas de entrada en palabras por el elemento incorporado read.

Permítanme tratar de aclarar esto. Creo que podría ser bueno hacer una distinción entre parsingy execution. Bash debe primero analizar el código fuente, que obviamente es un evento analizar, y luego ejecutar el código, que es cuando la expansión entra en escena. La expansión es realmente un evento de ejecución. Además, estoy en desacuerdo con la descripción de la variable $IFS que acabo de citar anteriormente; en lugar de decir que la división de palabras se realiza después de la expansión, yo diría que la división de palabras se realiza durante la expansión, o, quizás aún más precisamente, la división de palabras es parte de el proceso de expansión. La frase "división de palabras" se refiere solo a este paso de expansión; nunca debe usarse para referirse al análisis del código fuente de bash, aunque desafortunadamente los documentos parecen para lanzar alrededor de las palabras "dividir" y "palabras" mucho. He aquí un extracto relevante del linux.die.net versión del manual bash:

La expansión se realiza en la línea de comandos después de que se ha dividido en palabras. Hay siete tipos de expansión realizado: llave de expansión, tilde expansión, parámetro y variable de expansión, la sustitución de comandos, la expansión aritmética, palabra dividir , y expansión de ruta.

El orden de las expansiones es: expansión de llaves; expansión de tilde, expansión de parámetros y variables, expansión aritmética y sustitución de comandos (hecha de izquierda a derecha); división de palabras; y expansión de nombres de ruta.

Se podría argumentar que la versión GNU del manual hace un poco mejor, ya que opta por la palabra "tokens" en lugar de "words" en la primera frase de la Expansión sección:

La expansión se realiza en la línea de comandos después de que se ha dividido en tokens.

El punto importante es, $IFS no cambia la forma en que bash analiza el código fuente. El análisis del código fuente de bash es en realidad un proceso muy complejo que implica el reconocimiento de los diversos elementos de la gramática del shell, como secuencias de comandos, listas de comandos, tuberías, expansiones de parámetros, sustituciones aritméticas y sustituciones de comandos. En su mayor parte, el el proceso de análisis bash no puede ser alterado por acciones a nivel de usuario como asignaciones de variables (en realidad, hay algunas excepciones menores a esta regla; por ejemplo, vea los compatxx shell settings , que puede cambiar ciertos aspectos del comportamiento de análisis sobre la marcha). Las "palabras"/"tokens" aguas arriba que resultan de este complejo proceso de análisis se expanden de acuerdo con el proceso general de "expansión" como se desglosa en los extractos de la documentación anterior, donde la división de palabras de la expanded (expanding?) texto en palabras aguas abajo es simplemente un paso de ese proceso. La división de palabras solo toca el texto que se ha escupido de un paso de expansión anterior; no afecta al texto literal que se analizó directamente del bytestream de origen.


respuesta Incorrecta #7

string='first line
        second line
        third line'

while read -r line; do lines+=("$line"); done <<<"$string"

Esta es una de las mejores soluciones. Observe que estamos de vuelta a usar read. ¿No dije antes que read es inapropiado porque realiza dos niveles de división, cuando solo necesitamos uno? El truco aquí es que puede llamar read de tal manera que efectivamente solo hace un nivel de división, específicamente dividiendo solo un campo por invocación, lo que requiere el costo de tener que llamarlo repetidamente en un bucle. Es un poco de juego de manos, pero funciona.

Pero hay problemas. Primero: Cuando proporciona al menos un argumento NAME a read, ignora automáticamente el interlineado y espacio en blanco final en cada campo que se separa de la cadena de entrada. Esto ocurre si $IFS está establecido en su valor predeterminado o no, como se describió anteriormente en este post. Ahora, el OP puede no preocuparse por esto para su caso de uso específico, y de hecho, puede ser una característica deseable del comportamiento de análisis. Pero no todos los que quieran analizar una cadena en campos querrán esto. Hay una solución, sin embargo: Un uso algo no obvio de read es pasar cero NAME argumentos. En en este caso, read almacenará toda la línea de entrada que obtiene del flujo de entrada en una variable llamada $REPLY, y, como beneficio adicional, no elimina los espacios en blanco iniciales y finales del valor. Este es un uso muy robusto de read que he explotado con frecuencia en mi carrera de programación de shell. He aquí una demostración de la diferencia en el comportamiento:

string=$'  a  b  \n  c  d  \n  e  f  '; ## input string

a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -p a;
## declare -a a=([0]="a  b" [1]="c  d" [2]="e  f") ## read trimmed surrounding whitespace

a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="  a  b  " [1]="  c  d  " [2]="  e  f  ") ## no trimming

El segundo problema con esta solución es que en realidad no aborda el caso de un campo personalizado separador, como el espacio de coma de la OP. Como antes, los separadores de múltiples caracteres no son compatibles, lo que es una limitación desafortunada de esta solución. Podríamos intentar al menos dividir en coma especificando el separador a la opción -d, pero mira lo que sucede:

string='Paris, France, Europe';
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France")

Como era de esperar, el espacio en blanco circundante no contabilizado se introdujo en los valores del campo, y por lo tanto esto tendría que corregirse posteriormente mediante operaciones de recorte (esto también se podría hacer directamente en while-loop). Pero hay otro error obvio: ¡Falta Europa! ¿Qué le pasó? La respuesta es que read devuelve un código de retorno fallido si llega al final del archivo (en este caso podemos llamarlo fin de cadena) sin encontrar un terminador de campo final en el campo final. Esto hace que el bucle while se rompa prematuramente y perdamos el campo final.

Técnicamente este mismo error también afectó a los ejemplos anteriores; la diferencia es que se tomó el separador de campos ser LF, que es el valor predeterminado cuando no se especifica la opción -d, y el mecanismo <<< ("here-string") agrega automáticamente un LF a la cadena justo antes de que la alimente como entrada al comando. Por lo tanto, en esos casos, una especie de accidentalmente resuelto el problema de un campo final caído mediante la adición involuntaria de un terminador ficticio adicional a la entrada. Llamemos a esta solución la solución "dummy-terminator". Podemos aplicar la solución dummy-terminator manualmente para cualquier delimitador personalizado concatenándolo contra la cadena de entrada nosotros mismos al instanciarlo en la cadena here-string:

a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

Ahí, problema resuelto. Otra solución es romper el bucle while solo si (1) read devuelve un error y (2) $REPLY está vacío, lo que significa que read no fue capaz de leer ningún carácter antes de golpear el final del archivo. Demo:

a=(); while read -rd,|| [[ -n "$REPLY" ]]; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

Este enfoque también revela el secreto LF que automáticamente se agrega a la cadena here por el <<< operador de redirección. Por supuesto, podría ser despojado por separado a través de una operación de recorte explícita como se describió hace un momento, pero obviamente el enfoque manual dummy-terminator lo resuelve directamente, por lo que podríamos ir con eso. La solución manual dummy-terminator es realmente muy conveniente ya que resuelve ambos de estos dos problemas (el problema dropped-final-field y el problema appended-LF) de una sola vez.

Así que, en general, esta es una solución bastante poderosa. Es sólo la debilidad restante es la falta de apoyo a los delimitadores de caracteres múltiples, que abordaré más adelante.


respuesta Incorrecta #8

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

(Esto es en realidad del mismo post que #7; el responsable proporcionó dos soluciones en el mismo puesto.)

El builtin readarray, que es sinónimo de mapfile, es ideal. Es un comando incorporado que analiza un bytestream en una variable de matriz en una sola toma; sin jugar con bucles, condicionales, sustituciones o cualquier otra cosa. Y no elimina subrepticiamente ningún espacio en blanco de la cadena de entrada. Y (si no se da -O) limpia convenientemente el array de destino antes de asignarlo. Pero todavía no es perfecto, de ahí mi crítica de ella como una "respuesta incorrecta".

Primero, solo para sacar esto del camino, tenga en cuenta que, al igual que el comportamiento de read al hacer el análisis de campos, readarray elimina el campo final si está vacío. Una vez más, esto probablemente no una preocupación para el OP, pero podría ser para algunos casos de uso. Volveré a esto en un momento.

En segundo lugar, como antes, no soporta delimitadores de múltiples caracteres. Voy a dar una solución para esto en un momento también.

Tercero, la solución tal como está escrita no analiza la cadena de entrada del OP, y de hecho, no se puede usar tal cual para analizarla. Voy a ampliar en este momento también.

Por las razones anteriores, todavía considero que esta es una "respuesta incorrecta" a la OP pregunta. A continuación voy a dar lo que considero que es la respuesta correcta.


Respuesta Correcta

Aquí hay un intento ingenuo de hacer #8 trabaje simplemente especificando la opción -d:

string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

Vemos que el resultado es idéntico al resultado que obtuvimos del enfoque de doble condicional de la solución de bucle read discutida en #7. Podemos casi resolver esto con el manual dummy-terminator truco:

readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')

El problema aquí es que readarray preservó el campo final, ya que el operador de redirección <<< agregó el LF a la cadena de entrada, y por lo tanto el campo final estaba no vacío (de lo contrario se habría eliminado). Podemos ocuparnos de esto desactivando explícitamente el elemento final del array después del hecho:

readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

Los únicos dos problemas que quedan, que en realidad están relacionados, son (1) el espacio en blanco extraño que necesita ser recortado, y (2) la falta de soporte para delimitadores de múltiples caracteres.

El espacio en blanco podría, por supuesto, ser recortado después (por ejemplo, ver Cómo recortar el espacio en blanco de una variable Bash?). Pero si podemos hackear un delimitador de múltiples caracteres, entonces eso resolvería ambos problemas de una sola vez.

Desafortunadamente, no hay una forma directa de hacer que funcione un delimitador de múltiples caracteres. La mejor solución que he pensado es preprocesar la cadena de entrada para reemplazar el delimitador de múltiples caracteres con un delimitador de un solo carácter que se garantizará que no colisione con el contenido de la cadena de entrada. El único carácter que tiene esta garantía es elNUL byte . Esto se debe a que, en bash (aunque no en zsh, por cierto), las variables no pueden contener el byte NUL. Este paso de preprocesamiento se puede realizar en línea en una sustitución de proceso. He aquí cómo hacerlo usando awk :

readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; }' <<<"$string, "); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

Allí, finalmente! Esta solución no dividir erróneamente los campos en el medio, no recortará prematuramente, no soltará campos vacíos, no se corromperá en expansiones de nombres de archivo, no eliminará automáticamente los espacios en blanco iniciales y finales, no dejará un stowaway LF al final, no requiere bucles y no se conforma con un delimitador de un solo carácter.


Solución de recorte

Por último, quería demostrar mi propia solución de recorte bastante intrincada usando el oscuro {[129]]} opción de readarray. Desafortunadamente, me he quedado sin espacio contra el draconiano límite de publicación de 30.000 personajes de Stack Overflow, por lo que no podré explicarlo. Dejaré eso como un ejercicio para el lector.

function mfcb { local val="$4"; "$1"; eval "$2[$3]=\$val;"; };
function val_ltrim { if [[ "$val" =~ ^[[:space:]]+ ]]; then val="${val:${#BASH_REMATCH[0]}}"; fi; };
function val_rtrim { if [[ "$val" =~ [[:space:]]+$ ]]; then val="${val:0:${#val}-${#BASH_REMATCH[0]}}"; fi; };
function val_trim { val_ltrim; val_rtrim; };
readarray -c1 -C 'mfcb val_trim a' -td, <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")
 149
Author: bgoldst,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-20 15:49:24
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

Imprime tres

 46
Author: Jmoney38,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-06 17:51:37

A veces me pasaba que el método descrito en la respuesta aceptada no funcionaba, especialmente si el separador es un retorno de carro.
En esos casos resolví de esta manera:

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

for line in "${lines[@]}"
    do
        echo "--> $line"
done
 29
Author: Luca Borrione,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-11-02 13:44:37

La respuesta aceptada funciona para valores en una línea.
Si la variable tiene varias líneas:

string='first line
        second line
        third line'

Necesitamos un comando muy diferente para obtener todas las líneas:

while read -r line; do lines+=("$line"); done <<<"$string"

O el mucho más simple bash readarray :

readarray -t lines <<<"$string"

Imprimir todas las líneas es muy fácil aprovechando una función printf:

printf ">[%s]\n" "${lines[@]}"

>[first line]
>[        second line]
>[        third line]
 23
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-24 21:24:27

Esto es similar al enfoque de Jmoney38, pero usando sed:

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
echo ${array[0]}

Impresiones 1

 4
Author: ssanch,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-06-03 15:24:19

La clave para dividir la cadena en una matriz es el delimitador de caracteres múltiples de ", ". Cualquier solución que use IFS para delimitadores de caracteres múltiples es inherentemente incorrecta ya que IFS es un conjunto de esos caracteres, no una cadena.

Si asigna IFS=", " entonces la cadena se romperá en "," O " " o cualquier combinación de ellos que no sea una representación precisa del delimitador de dos caracteres de ", ".

Puede usar awk o sed para dividir la cadena, con process sustitución:

#!/bin/bash

str="Paris, France, Europe"
array=()
while read -r -d $'\0' each; do   # use a NUL terminated field separator 
    array+=("$each")
done < <(printf "%s" "$str" | awk '{ gsub(/,[ ]+|$/,"\0"); print }')
declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output

Es más eficiente usar una expresión regular directamente en Bash:

#!/bin/bash

str="Paris, France, Europe"

array=()
while [[ $str =~ ([^,]+)(,[ ]+|$) ]]; do
    array+=("${BASH_REMATCH[1]}")   # capture the field
    i=${#BASH_REMATCH}              # length of field + delimiter
    str=${str:i}                    # advance the string by that length
done                                # the loop deletes $str, so make a copy if needed

declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output...

Con la segunda forma, no hay sub shell y será inherentemente más rápido.


Edit by bgoldst: Aquí hay algunos puntos de referencia que comparan mi solución readarray con la solución regex de dawg, y también incluí la solución read para el heck de la misma (nota: Modifiqué ligeramente la solución regex para una mayor armonía con mi solución) (también ver mis comentarios post):

## competitors
function c_readarray { readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); unset 'a[-1]'; };
function c_read { a=(); local REPLY=''; while read -r -d ''; do a+=("$REPLY"); done < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); };
function c_regex { a=(); local s="$1, "; while [[ $s =~ ([^,]+),\  ]]; do a+=("${BASH_REMATCH[1]}"); s=${s:${#BASH_REMATCH}}; done; };

## helper functions
function rep {
    local -i i=-1;
    for ((i = 0; i<$1; ++i)); do
        printf %s "$2";
    done;
}; ## end rep()

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "$1" != ':' ]]; do
        func="$1";
        if [[ ! "$func" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then
            echo "bad function name: $func" >&2;
            return 2;
        fi;
        funcs+=("$func");
        shift;
    done;
    shift;
    args=("$@");
    for func in "${funcs[@]}"; do
        echo -n "$func ";
        { time $func "${args[@]}" >/dev/null 2>&1; } 2>&1| tr '\n' '/';
        rc=${PIPESTATUS[0]}; if [[ $rc -ne 0 ]]; then echo "[$rc]"; else echo; fi;
    done| column -ts/;
}; ## end testAll()

function makeStringToSplit {
    local -i n=$1; ## number of fields
    if [[ $n -lt 0 ]]; then echo "bad field count: $n" >&2; return 2; fi;
    if [[ $n -eq 0 ]]; then
        echo;
    elif [[ $n -eq 1 ]]; then
        echo 'first field';
    elif [[ "$n" -eq 2 ]]; then
        echo 'first field, last field';
    else
        echo "first field, $(rep $[$1-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

function testAll_splitIntoArray {
    local -i n=$1; ## number of fields in input string
    local s='';
    echo "===== $n field$(if [[ $n -ne 1 ]]; then echo 's'; fi;) =====";
    s="$(makeStringToSplit "$n")";
    testAll c_readarray c_read c_regex : "$s";
}; ## end testAll_splitIntoArray()

## results
testAll_splitIntoArray 1;
## ===== 1 field =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.000s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 10;
## ===== 10 fields =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.001s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 100;
## ===== 100 fields =====
## c_readarray   real  0m0.069s   user 0m0.000s   sys  0m0.062s
## c_read        real  0m0.065s   user 0m0.000s   sys  0m0.046s
## c_regex       real  0m0.005s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 1000;
## ===== 1000 fields =====
## c_readarray   real  0m0.084s   user 0m0.031s   sys  0m0.077s
## c_read        real  0m0.092s   user 0m0.031s   sys  0m0.046s
## c_regex       real  0m0.125s   user 0m0.125s   sys  0m0.000s
##
testAll_splitIntoArray 10000;
## ===== 10000 fields =====
## c_readarray   real  0m0.209s   user 0m0.093s   sys  0m0.108s
## c_read        real  0m0.333s   user 0m0.234s   sys  0m0.109s
## c_regex       real  0m9.095s   user 0m9.078s   sys  0m0.000s
##
testAll_splitIntoArray 100000;
## ===== 100000 fields =====
## c_readarray   real  0m1.460s   user 0m0.326s   sys  0m1.124s
## c_read        real  0m2.780s   user 0m1.686s   sys  0m1.092s
## c_regex       real  17m38.208s   user 15m16.359s   sys  2m19.375s
##
 2
Author: dawg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-27 04:36:33

Prueba esto

IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done

Es simple. Si lo desea, también puede agregar una declaración (y también eliminar las comas):

IFS=' ';declare -a array=(Paris France Europe)

El IFS se agrega para deshacer lo anterior, pero funciona sin él en una nueva instancia de bash

 1
Author: Geoff Lee,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-04 06:02:07

Usa esto:

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe
 0
Author: Eduardo Cuomo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-19 19:00:33

¡Aquí está mi truco!

Dividir cadenas por cadenas es algo bastante aburrido de hacer usando bash. Lo que sucede es que tenemos enfoques limitados que solo funcionan en unos pocos casos (divididos por ";", "/", "."y así sucesivamente) o tenemos una variedad de efectos secundarios en las salidas.

El enfoque a continuación ha requerido una serie de maniobras, pero creo que funcionará para la mayoría de nuestras necesidades!

#!/bin/bash

# --------------------------------------
# SPLIT FUNCTION
# ----------------

F_SPLIT_R=()
f_split() {
    : 'It does a "split" into a given string and returns an array.

    Args:
        TARGET_P (str): Target string to "split".
        DELIMITER_P (Optional[str]): Delimiter used to "split". If not 
    informed the split will be done by spaces.

    Returns:
        F_SPLIT_R (array): Array with the provided string separated by the 
    informed delimiter.
    '

    F_SPLIT_R=()
    TARGET_P=$1
    DELIMITER_P=$2
    if [ -z "$DELIMITER_P" ] ; then
        DELIMITER_P=" "
    fi

    REMOVE_N=1
    if [ "$DELIMITER_P" == "\n" ] ; then
        REMOVE_N=0
    fi

    # NOTE: This was the only parameter that has been a problem so far! 
    # By Questor
    # [Ref.: https://unix.stackexchange.com/a/390732/61742]
    if [ "$DELIMITER_P" == "./" ] ; then
        DELIMITER_P="[.]/"
    fi

    if [ ${REMOVE_N} -eq 1 ] ; then

        # NOTE: Due to bash limitations we have some problems getting the 
        # output of a split by awk inside an array and so we need to use 
        # "line break" (\n) to succeed. Seen this, we remove the line breaks 
        # momentarily afterwards we reintegrate them. The problem is that if 
        # there is a line break in the "string" informed, this line break will 
        # be lost, that is, it is erroneously removed in the output! 
        # By Questor
        TARGET_P=$(awk 'BEGIN {RS="dn"} {gsub("\n", "3F2C417D448C46918289218B7337FCAF"); printf $0}' <<< "${TARGET_P}")

    fi

    # NOTE: The replace of "\n" by "3F2C417D448C46918289218B7337FCAF" results 
    # in more occurrences of "3F2C417D448C46918289218B7337FCAF" than the 
    # amount of "\n" that there was originally in the string (one more 
    # occurrence at the end of the string)! We can not explain the reason for 
    # this side effect. The line below corrects this problem! By Questor
    TARGET_P=${TARGET_P%????????????????????????????????}

    SPLIT_NOW=$(awk -F"$DELIMITER_P" '{for(i=1; i<=NF; i++){printf "%s\n", $i}}' <<< "${TARGET_P}")

    while IFS= read -r LINE_NOW ; do
        if [ ${REMOVE_N} -eq 1 ] ; then

            # NOTE: We use "'" to prevent blank lines with no other characters 
            # in the sequence being erroneously removed! We do not know the 
            # reason for this side effect! By Questor
            LN_NOW_WITH_N=$(awk 'BEGIN {RS="dn"} {gsub("3F2C417D448C46918289218B7337FCAF", "\n"); printf $0}' <<< "'${LINE_NOW}'")

            # NOTE: We use the commands below to revert the intervention made 
            # immediately above! By Questor
            LN_NOW_WITH_N=${LN_NOW_WITH_N%?}
            LN_NOW_WITH_N=${LN_NOW_WITH_N#?}

            F_SPLIT_R+=("$LN_NOW_WITH_N")
        else
            F_SPLIT_R+=("$LINE_NOW")
        fi
    done <<< "$SPLIT_NOW"
}

# --------------------------------------
# HOW TO USE
# ----------------

STRING_TO_SPLIT="
 * How do I list all databases and tables using psql?

\"
sudo -u postgres /usr/pgsql-9.4/bin/psql -c \"\l\"
sudo -u postgres /usr/pgsql-9.4/bin/psql <DB_NAME> -c \"\dt\"
\"

\"
\list or \l: list all databases
\dt: list all tables in the current database
\"

[Ref.: https://dba.stackexchange.com/questions/1285/how-do-i-list-all-databases-and-tables-using-psql]


"

f_split "$STRING_TO_SPLIT" "bin/psql -c"

# --------------------------------------
# OUTPUT AND TEST
# ----------------

ARR_LENGTH=${#F_SPLIT_R[*]}
for (( i=0; i<=$(( $ARR_LENGTH -1 )); i++ )) ; do
    echo " > -----------------------------------------"
    echo "${F_SPLIT_R[$i]}"
    echo " < -----------------------------------------"
done

if [ "$STRING_TO_SPLIT" == "${F_SPLIT_R[0]}bin/psql -c${F_SPLIT_R[1]}" ] ; then
    echo " > -----------------------------------------"
    echo "The strings are the same!"
    echo " < -----------------------------------------"
fi
 0
Author: Eduardo Lucio,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-31 20:45:59

Otra forma de hacerlo sin modificar IFS:

read -r -a myarray <<< "${string//, /$IFS}"

En lugar de cambiar IFS para que coincida con nuestro delimitador deseado, podemos reemplazar todas las ocurrencias de nuestro delimitador deseado ", " con el contenido de $IFS a través de "${string//, /$IFS}".

Tal vez esto será lento para cuerdas muy grandes sin embargo?

Esto se basa en la respuesta de Dennis Williamson.

 0
Author: sel-en-ium,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-31 05:56:22

Otro enfoque puede ser:

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

Después de esto 'arr' es una matriz con cuatro cadenas. Esto no requiere tratar IFS o leer o cualquier otra cosa especial por lo tanto mucho más simple y directa.

 -1
Author: rsjethani,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-13 16:21:10

ACTUALIZACIÓN: No hagas esto, debido a problemas con eval.

Con un poco menos de ceremonia:

IFS=', ' eval 'array=($string)'

Por ejemplo

string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar
 -1
Author: user1009908,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-06 23:18:44

Otra forma sería:

string="Paris, France, Europe"
IFS=', ' arr=(${string})

Ahora sus elementos se almacenan en la matriz "arr". Para iterar a través de los elementos:

for i in ${arr[@]}; do echo $i; done
 -1
Author: Safter Arslan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-09 03:21:05