Dividir la cadena en una matriz en Bash

641

En un script Bash, me gustaría dividir una línea en pedazos y almacenarlos 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 un código simple, la velocidad del comando no importa. ¿Cómo puedo hacerlo?

Lgn
fuente
22
Este es el éxito número 1 de Google, pero hay una controversia en la respuesta porque, desafortunadamente, la pregunta se refiere a la delimitación de , (coma-espacio) y no a un solo carácter como la coma. Si solo está interesado en lo último, las respuestas aquí son más fáciles de seguir: stackoverflow.com/questions/918886/…
antak
Si desea combinar una cadena y no le importa tenerla como una matriz, cutes un comando bash útil a tener en cuenta también. El separador es definible en.wikibooks.org/wiki/Cut También puede extraer datos de una estructura de registro de ancho fijo. en.wikipedia.org/wiki/Cut_(Unix) computerhope.com/unix/ucut.htm
JGFMK

Respuestas:

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

Tenga en cuenta que los personajes $IFSson tratados de forma individual como separadores de manera que en este caso los campos se pueden separar por cualquiera 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 el espacio de coma aparece 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 las matrices de Bash son escasas. 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 una matriz:

echo "${#array[@]}"

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

echo "${array[-1]}"

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

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

Las compensaciones negativas más grandes se seleccionan más lejos del final de la matriz. Tenga en cuenta el espacio antes del signo menos en la forma anterior. Es requerido.

Pausado hasta nuevo aviso.
fuente
15
Simplemente use IFS=', ', entonces no tiene que eliminar los espacios por separado. Prueba:IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"
l0b0
44
@ l0b0: Gracias. No sé lo que estaba pensando. Me gusta usar declare -p arraypara la salida de prueba, por cierto.
Pausado hasta nuevo aviso.
1
Esto no parece respetar las citas. Por ejemplo, France, Europe, "Congo, The Democratic Republic of the"esto se dividirá después del congo.
Israel Dov
2
@YisraelDov: Bash no tiene forma de lidiar con CSV por sí mismo. No puede diferenciar entre comas dentro de comillas y las que están fuera de ellas. Deberá utilizar una herramienta que comprenda CSV, como una lib en un lenguaje de nivel superior, por ejemplo, el módulo csv en Python.
Pausado hasta nuevo aviso.
55
str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str"se dividirá en array=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")una nota. Por lo tanto, esto solo funciona con campos sin espacios, ya que IFS=', 'es un conjunto de caracteres individuales, no un delimitador de cadena.
dawg
333

Todas las respuestas a esta pregunta son incorrectas de una forma u otra.


Respuesta incorrecta # 1

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

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

En realidad, para los fanáticos reales, el significado completo de $IFSes un poco más complicado. 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 valor es exactamente <space><tab> <newline> , el valor predeterminado, entonces las secuencias de <space> , <tab> y <newline> al principio y al final de los resultados de las expansiones anteriores se ignoran, y cualquier secuencia de caracteres IFS que no se encuentre al principio o al final sirve para delimitar palabras. Si IFS tiene un valor diferente al predeterminado, entonces las secuencias de los caracteres de espacio en blanco <space> , <tab> 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 de IFS ). Cualquier carácter en IFS que no sea espacio en blanco IFS , junto con cualquier carácter de espacio en blanco IFS adyacente , delimita un campo. Una secuencia de caracteres de espacio en blanco IFS también se trata como un delimitador. Si el valor de IFS es nulo, no se divide la palabra.

Básicamente, para valores no predeterminados no nulos de $IFS, los campos se pueden separar con (1) una secuencia de uno o más caracteres que pertenecen al conjunto de "caracteres de espacio en blanco IFS" (es decir, cualquiera de <space> , <tab> y <newline> ("nueva línea", que significa avance de línea (LF) ) están presentes en cualquier lugar $IFS), o (2) cualquier carácter de "espacio en blanco IFS" que esté presente en$IFS junto con los "caracteres de espacio en blanco IFS" que 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é pasa si su cadena de entrada era '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 usted fuera a utilizar esta solución con un separador de un solo carácter (como una coma por sí mismo, es decir, sin espacio siguiente u otro equipaje), si el valor de la $stringvariable de pasa para contener cualquier LF, a continuación, readse deje de procesar una vez que encuentre el primer LF. El readbuiltin solo procesa una línea por invocación. Esto es cierto incluso si está canalizando o redirigiendo la entrada solo a la readdeclaración, como lo 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 impulsa el readincorporado no tiene conocimiento del flujo de datos dentro de su estructura de comando que contiene.

Podría argumentar que es poco probable que esto cause un problema, pero aún así, es un peligro sutil que debe evitarse si es posible. Es causada por el hecho de que la readconstrucción en realidad 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 readbuiltin no es apropiado, y debemos evitarlo.

3: Un problema potencial no obvio con esta solución es que readsiempre elimina el campo final si está vacío, aunque de lo contrario conserva los campos vacíos. 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 el OP no se preocuparía por 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 agregando un delimitador final falso a la cadena de entrada justo antes de alimentarlo 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: agregué los paréntesis faltantes alrededor de la sustitución del comando 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 readque la división de palabras general también utiliza la $IFSvariable especial, aunque en este caso se da a entender que se establece en su valor predeterminado de <space><tab> <newline> y, por lo tanto, cualquier secuencia de uno o más IFS los caracteres (que ahora son todos espacios en blanco) se consideran un delimitador de campo.

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 al igual que antes, el problema aquí es que los campos individuales en la cadena de entrada ya pueden contener $IFScaracteres y, por lo tanto, se dividirían incorrectamente durante la operación de división de palabras. Este no es el caso para 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 base de código que usara este idioma correría el riesgo de explotar si alguna vez se viola esta suposición en algún momento. Una vez más, considere mi contraejemplo de 'Los Angeles, United States, North America'(o 'Los Angeles:United States:North America').

También, la división de palabras es normalmente seguido por la expansión de nombre de archivo ( aka expansión nombre de ruta aka globbing), que, si se hace, se palabras potencialmente corruptos que contienen los caracteres *, ?o [seguido de ](y, si extglobse establece, los fragmentos entre paréntesis precedido de ?, *, +, @, o !) al compararlos con objetos del sistema de archivos y expandir las palabras ("globos") en consecuencia. El primero de estos tres respondedores ha socavado hábilmente este problema al ejecutar de set -fantemano para desactivar el bloqueo. Técnicamente esto funciona (aunque probablemente debería agregarset +f luego 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 o no ser un problema, dependiendo de la aplicación.

Nota: Si va a usar esta solución, es mejor usar la forma de ${string//:/ }"sustitución de patrón" de expansión de parámetros , en lugar de tener que molestarse en invocar una sustitución de comando (que bifurca el shell), iniciar una canalización y ejecutando un ejecutable externo ( tro sed), ya que la expansión de parámetros es puramente una operación interna del shell. (Además, para las soluciones try sed, la variable de entrada debe estar entre comillas dobles dentro de la sustitución del comando; de lo contrario, la división de palabras tendría efecto en el echocomando y potencialmente alteraría los valores del campo. Además, la $(...)forma de sustitución del comando es preferible a la anterior`...` formulario ya que simplifica el anidamiento de las sustituciones de comandos 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 la n . ° 2 . La diferencia es que el respondedor ha asumido que los campos están delimitados por dos caracteres, uno de los cuales está representado por defecto $IFSy 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 patrón y luego utilizando la división de palabras para dividir los campos en el carácter delimitador representado por IFS sobreviviente.

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 eliminarlo y luego, dependiendo del carácter de espacio para la división de campo, es simplemente incorrecto. Una vez más, tenga en cuenta mis contraejemplo: 'Los Angeles, United States, North America'.

Además, una vez más, la expansión del nombre de archivo podría corromper las palabras expandidas, pero esto se puede evitar deshabilitando temporalmente la asignación con set -fy luego set +f.

Además, nuevamente, se perderán todos los campos vacíos, 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 en que usa la división de palabras para hacer el trabajo, solo que ahora el código se establece explícitamente $IFSpara contener solo el delimitador de campo de un solo carácter presente en la cadena de entrada. Debe repetirse que esto no puede funcionar para delimitadores de campo de caracteres múltiples, 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 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 envolviendo la declaración crítica en set -fy 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 . Por supuesto, esto no sería un problema si el delimitador no es un "carácter de espacio en blanco IFS" y, dependiendo de la aplicación, puede no importar de todos modos, pero sí vicia la generalidad de la solución.

En resumen, suponiendo que tiene un delimitador de un carácter y que no es un "carácter de espacio en blanco IFS" o que no le interesan los campos vacíos y ajusta la declaración crítica set -fy set +f, entonces, esta solución funciona , pero por lo demás no.

(Además, por el bien de la información, la asignación de 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 el n . ° 1 (en que se establece $IFSen espacio de coma) y el n . ° 2-4 (en que usa la división de palabras para dividir la cadena en campos). Debido a esto, sufre la mayoría de los problemas que afectan a todas las respuestas incorrectas anteriores, algo así como el peor de todos los mundos.

Además, con respecto a la segunda variante, puede parecer que la evalllamada es completamente innecesaria, ya que su argumento es un literal de cadena entre comillas simples y, por lo tanto, es estáticamente conocido. Pero en realidad hay un beneficio muy obvio de usar evalde esta manera. Normalmente, cuando se ejecuta un comando simple que consiste en una asignación de variable única , es decir, sin una palabra de comando real que le sigue, 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 involucra múltiples asignaciones de variables; de nuevo, siempre que no haya una palabra de comando, todas las asignaciones de variables afectan el entorno del shell:

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

Pero, si la asignación de variables se adjunta a un nombre de comando (me gusta llamar a esto una "asignación de prefijo"), entonces no afecta el entorno de shell y, en cambio, solo afecta el entorno del comando ejecutado, independientemente de si es una función incorporada 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 relevante del manual de bash :

Si no se obtiene un nombre de comando, las asignaciones de variables afectan el entorno actual del shell. De lo contrario, las variables se agregan al entorno del comando ejecutado y no afectan el entorno actual del shell.

Es posible explotar esta característica de asignación de variables para cambiar $IFSsolo temporalmente, lo que nos permite evitar todo el gambito de guardar y restaurar como el que se está haciendo con la $OIFSvariable 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 de variables, y por lo tanto no implicaría una palabra de comando para hacer que la $IFSasignación sea temporal. Podrías pensar para ti mismo, bueno, ¿por qué no simplemente agregar una palabra de comando no-op a la declaración como : builtinpara hacer que la $IFSasignación sea temporal? Esto no funciona porque luego también haría que la $arrayasignación sea temporal:

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

Por lo tanto, estamos efectivamente en un punto muerto, un poco atrapados. Pero, cuando evalejecuta su código, lo ejecuta en el entorno de shell, como si fuera normal, código fuente estático, y por lo tanto podemos ejecutar la $arrayasignación dentro del evalargumento para que tenga efecto en el entorno de shell, mientras que la $IFSasignación de prefijo que está prefijado al evalcomando no sobrevivirá al evalcomando. Este es exactamente el truco que se está utilizando en la segunda variante de esta solución:

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

Entonces, 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 la asignación) de una manera bastante 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 la cadena de argumentos para protegerse contra las amenazas de seguridad.

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


Respuesta incorrecta # 6

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

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

¿Um que? El OP tiene una variable de cadena que debe analizarse 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 $IFSvariable afecta a todos los análisis de bash en todos los contextos, lo cual no es cierto. Del manual de bash:

IFS     El separador de campo interno que se usa para dividir palabras después de la expansión y para dividir líneas en palabras con el comando de lectura incorporado. El valor predeterminado es <space><tab> <newline> .

Por lo tanto, la $IFSvariable especial en realidad solo se usa en dos contextos: (1) división de palabras que se realiza después de la expansión (es decir, no al analizar el código fuente de bash) y (2) para dividir las líneas de entrada en palabras por el readincorporado.

Déjame intentar aclarar esto. Creo que podría ser bueno hacer una distinción entre análisis y ejecución . Bash primero debe analizar el código fuente, que obviamente es un evento de análisis , y luego ejecuta el código, que es cuando la expansión entra en escena. La expansión es realmente un evento de ejecución . Además, discrepo con la descripción de la $IFSvariable que acabo de citar arriba; 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 más precisamente, la división de palabras es parte deEl proceso de expansión. La frase "división de palabras" se refiere solo a este paso de expansión; nunca debería usarse para referirse al análisis del código fuente de bash, aunque desafortunadamente los documentos parecen arrojar muchas veces las palabras "dividir" y "palabras". Aquí hay un extracto relevante de la versión linux.die.net del manual bash:

La expansión se realiza en la línea de comando después de que se ha dividido en palabras. Hay siete tipos de expansión lleva a cabo: la expansión de llaves , de tilde de expansión , los parámetros y la expansión de variables , sustitución de orden , expansión aritmética , la división de palabras , y la expansión de nombre de camino .

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 (hecho de izquierda a derecha); división de palabras; y expansión de nombre de ruta.

Podría argumentar que la versión GNU del manual funciona un poco mejor, ya que opta por la palabra "tokens" en lugar de "palabras" en la primera oración de la sección Expansión:

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

El punto importante es $IFSque 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 de 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 proceso de análisis de 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 las diversas compatxxconfiguraciones de shell, que puede cambiar ciertos aspectos del comportamiento de análisis sobre la marcha). Las "palabras" / "tokens" ascendentes que resultan de este complejo proceso de análisis se expanden de acuerdo con el proceso general de "expansión" tal como se desglosa en los extractos de documentación anteriores, donde la división de palabras del texto expandido (¿expansivo?) En aguas abajo palabras 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 el texto literal que fue analizado directamente desde la fuente por testream.


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. Tenga en cuenta que hemos vuelto a usar read. ¿No dije antes que eso reades inapropiado porque realiza dos niveles de división, cuando solo necesitamos uno? El truco aquí es que puede llamar readde tal manera que efectivamente solo hace un nivel de división, específicamente al dividir solo un campo por invocación, lo que requiere el costo de tener que llamarlo repetidamente en un bucle. Es un juego de manos, pero funciona.

Pero hay problemas. Primero: cuando proporciona al menos un argumento NAME para read, ignora automáticamente los espacios en blanco iniciales y finales en cada campo que se separa de la cadena de entrada. Esto ocurre independientemente de si $IFSse establece en su valor predeterminado o no, como se describió anteriormente en esta publicación. Ahora, el OP puede no importarle esto por 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 los campos querrán esto. Sin embargo, hay una solución: un uso algo no obvio de reades pasar cero argumentos NAME . En este caso, readalmacenará toda la línea de entrada que obtiene de la secuencia de entrada en una variable denominada $REPLYy, como beneficio adicional, nosepare los espacios en blanco iniciales y finales del valor. Este es un uso muy robusto readque he explotado con frecuencia en mi carrera de programación de shell. Aquí hay una demostración de la diferencia de 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 separador de campo personalizado, como el espacio de coma del OP. Como antes, los separadores de caracteres múltiples no son compatibles, lo cual es una limitación desafortunada de esta solución. Podríamos intentar al menos dividirnos en comas especificando el separador de la -dopción, 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 de campo y, por lo tanto, esto tendría que corregirse posteriormente mediante operaciones de recorte (esto también podría hacerse directamente en el bucle while). Pero hay otro error obvio: ¡falta Europa! ¿Que le paso a eso? La respuesta es que readdevuelve un código de retorno fallido si llega al final del archivo (en este caso podemos llamarlo final de la cadena) sin encontrar un terminador de campo final en el campo final. Esto hace que el ciclo while se rompa prematuramente y perdamos el campo final.

Técnicamente, este mismo error también afectaba a los ejemplos anteriores; la diferencia es que el separador de campo se consideró LF, que es el valor predeterminado cuando no especifica la -dopción, y el <<<mecanismo ("here-string") agrega automáticamente un LF a la cadena justo antes de que se alimente como entrada al comando. Por lo tanto, en esos casos, solucionamos accidentalmente el problema de un campo final eliminado agregando involuntariamente un terminador ficticio adicional a la entrada. Llamemos a esta solución la solución "dummy-terminator". Podemos aplicar la solución de terminación ficticia manualmente para cualquier delimitador personalizado concatenando contra la cadena de entrada nosotros mismos al instanciarla en la cadena here:

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 solo romper el ciclo while si tanto (1) readdevolvió el error como (2) $REPLYestá vacío, lo que significa que readno pudo leer ningún carácter antes de tocar el final del archivo. Manifestación:

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 LF secreto que el <<<operador de redireccionamiento agrega automáticamente a la cadena aquí . Por supuesto, podría eliminarse por separado a través de una operación de recorte explícita como se describió hace un momento, pero obviamente el enfoque manual de terminador ficticio lo resuelve directamente, por lo que podríamos seguir con eso. La solución manual de terminación ficticia es realmente bastante conveniente ya que resuelve ambos problemas (el problema de campo final descartado y el problema de LF adjunto) de una sola vez.

Entonces, en general, esta es una solución bastante poderosa. Su única debilidad es la falta de soporte para delimitadores de múltiples caracteres, que abordaré más adelante.


Respuesta incorrecta # 8

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

(Esto es en realidad de la misma publicación que # 7 ; el respondedor proporcionó dos soluciones en la misma publicación).

La readarrayconstrucción, que es sinónimo de mapfile, es ideal. Es un comando incorporado que analiza un bytestream en una variable de matriz de una sola vez; 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 -Ono se proporciona) borra convenientemente la matriz de destino antes de asignarla. Pero todavía no es perfecto, de ahí mi crítica de ello como una "respuesta incorrecta".

Primero, solo para sacar esto del camino, tenga en cuenta que, al igual que el comportamiento de readcuando se analiza el campo, se readarrayelimina el campo final si está vacío. Nuevamente, esto probablemente no sea una preocupación para el OP, pero podría serlo para algunos casos de uso. Volveré a esto en un momento.

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

En tercer lugar, la solución tal como está escrita no analiza la cadena de entrada del OP y, de hecho, no se puede usar como está para analizarla. Ampliaré esto momentáneamente también.

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


Respuesta correcta

Aquí hay un intento ingenuo de hacer que el # 8 funcione simplemente especificando la -dopción:

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 condicional doble de la readsolución de bucle discutido en el n . ° 7 . Casi podemos resolver esto con el truco manual del terminador ficticio:

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

El problema aquí es que readarrayconservó el campo final, ya que el <<<operador de redireccionamiento agregó el LF a la cadena de entrada y, por lo tanto, el campo final no estaba vacío (de lo contrario, se habría eliminado). Podemos ocuparnos de esto desarmando explícitamente el elemento de matriz final 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 caracteres múltiples.

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

Desafortunadamente, no hay una forma directa de hacer que funcione un delimitador de caracteres múltiples. La mejor solución que he pensado es preprocesar la cadena de entrada para reemplazar el delimitador de caracteres múltiples con un delimitador de un solo carácter que se garantizará que no colisionen con el contenido de la cadena de entrada. El único carácter que tiene esta garantía es el byte NUL . 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. Aquí se explica cómo hacerlo con awk :

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

¡Por fin! Esta solución no dividirá erróneamente los campos en el medio, no se cortará prematuramente, no dejará caer campos vacíos, no se corromperá en las expansiones de nombre de archivo, no eliminará automáticamente los espacios en blanco iniciales y finales, no dejará un LF polizón al final, no requiere bucles y no se conforma con un delimitador de un solo carácter.


Solución de corte

Por último, quería demostrar mi propia solución de recorte bastante compleja utilizando la oscura -C callbackopción de readarray. Desafortunadamente, me he quedado sin espacio contra el límite draconiano de 30,000 caracteres de Stack Overflow, por lo que no podré explicarlo. Lo dejaré como 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")
bgoldst
fuente
8
También puede ser útil tener en cuenta (aunque es comprensible que no haya espacio para hacerlo) que la -dopción de readarrayaparecer por primera vez en Bash 4.4.
fbicknel
2
Gran respuesta (+1). Si cambia su awk awk '{ gsub(/,[ ]+|$/,"\0"); print }'y elimina esa concatenación de la final, ", " entonces no tiene que pasar por la gimnasia para eliminar el registro final. Entonces: readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")en Bash que es compatible readarray. Tenga en cuenta que su método es Bash 4.4+ Creo que debido a la -denreadarray
Dawg
3
@datUser Eso es lamentable. Su versión de bash debe ser demasiado antigua para readarray. En este caso, puede usar la segunda mejor solución basada en read. Me refiero a esto: a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,";(con la awksustitución si necesita soporte delimitador de caracteres múltiples). Déjame saber si surge algún problema; Estoy bastante seguro de que esta solución debería funcionar en versiones bastante antiguas de bash, volviendo a la versión 2-algo, lanzada como hace dos décadas.
bgoldst
1
¡Vaya, qué respuesta tan brillante! Ji, ji, mi respuesta: ¡abandoné el script bash y activé Python!
artfulrobot
1
@datUser bash en OSX todavía está atascado en 3.2 (lanzado alrededor de 2007); He usado el bash encontrado en Homebrew para obtener versiones de bash 4.X en OS X
JDS
222

Aquí hay una manera sin configurar 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 reemplazo de cadena:

${string//substring/replacement}

para reemplazar todas las coincidencias de $ 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 hacer una pausa en la búsqueda de este script.

Jim Ho
fuente
1
Usé este enfoque ... hasta que me encontré con una larga cuerda para dividir. 100% de CPU por más de un minuto (luego lo maté). Es una pena porque este método permite dividir por una cadena, no por algún carácter en IFS.
Werner Lehmann
El 100% del tiempo de CPU por más de un minuto me parece que debe haber algo mal en alguna parte. ¿Cuánto tiempo duraba esa cadena, tiene un tamaño de MB o GB? Creo que, normalmente, si solo necesita una pequeña división de cadena, desea permanecer dentro de Bash, pero si es un archivo enorme, ejecutaría algo como Perl para hacerlo.
12
ADVERTENCIA: acabo de encontrar un problema con este enfoque. Si tiene un elemento llamado *, también obtendrá todos los elementos de su cwd. por lo tanto string = "1: 2: 3: 4: *" dará algunos resultados inesperados y posiblemente peligrosos dependiendo de su implementación. No obtuve el mismo error con (IFS = ',' read -a array <<< "$ string") y este parece seguro de usar.
Dieter Gribnitz
44
las citas ${string//:/ }evitan la expansión de shell
Andrew White
1
Tuve que usar lo siguiente en OSX: array=(${string//:/ })
Mark Thomson
95
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

Imprime tres

Jmoney38
fuente
8
De hecho, prefiero este enfoque. Simple.
shrimpwagon
44
Copié y pegué esto y no funcionó con echo, pero funcionó cuando lo usé en un bucle for.
Ben
2
Esto no funciona como se indica. @ Jmoney38 o shrimpwagon si puede pegar esto en un terminal y obtener el resultado deseado, pegue el resultado aquí.
abalter
2
@abalter trabaja para mí con a=($(echo $t | tr ',' "\n")). Mismo resultado con a=($(echo $t | tr ',' ' ')).
hoja
@procrastinator Lo acabo de probar VERSION="16.04.2 LTS (Xenial Xerus)"en un bashshell, y el último echosolo imprime una línea en blanco. ¿Qué versión de Linux y qué shell estás usando? Desafortunadamente, no se puede mostrar la sesión de terminal en un comentario.
abulter
29

A veces me ocurrió que el método descrito en la respuesta aceptada no funcionó, 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
Luca Borrione
fuente
2
+1 Esto funcionó completamente para mí. Necesitaba poner varias cadenas, divididas por una nueva línea, en una matriz, y read -a arr <<< "$strings"no funcionó IFS=$'\n'.
Stefan van den Akker
Esto no responde a la pregunta original.
Mike
29

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 la función printf:

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

>[first line]
>[        second line]
>[        third line]

fuente
2
Si bien no todas las soluciones funcionan para cada situación, su mención de readarray ... reemplazó mis últimas dos horas con 5 minutos ... obtuvo mi voto
Angry 84
7

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

ssanch
fuente
1
imprime 1 2 3 4 en mi caso
minigeek
6

La clave para dividir su cadena en una matriz es el delimitador de caracteres múltiples de ", ". Cualquier solución usandoIFS 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 awko sedpara dividir la cadena, con la sustitución del proceso:

#!/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.


Editar por bgoldst: Aquí hay algunos puntos de referencia que comparan mi readarraysolución con la solución regex de dawg, y también incluí la readsolución para el gusto (nota: modifiqué ligeramente la solución regex para una mayor armonía con mi solución) (también vea mis comentarios debajo del enviar):

## 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
##
perro
fuente
¡Muy buena solución! Nunca pensé en usar un bucle en una coincidencia de expresiones regulares, uso ingenioso de $BASH_REMATCH. Funciona y, de hecho, evita generar subcapas. +1 de mi parte Sin embargo, a modo de crítica, la expresión regular en sí misma es un poco no ideal, ya que parece que se vio obligado a duplicar parte del token delimitador (específicamente la coma) para evitar la falta de soporte para multiplicadores no codiciosos (también mira alrededor) en ERE (sabor de expresión regular "extendido" integrado en bash). Esto lo hace un poco menos genérico y robusto.
bgoldst
En segundo lugar, realicé algunas evaluaciones comparativas, y aunque el rendimiento es mejor que las otras soluciones para cuerdas más pequeñas, empeora exponencialmente debido a la repetida reconstrucción de cuerdas, volviéndose catastrófico para cuerdas muy grandes. Vea mi edición a su respuesta.
bgoldst
@bgoldst: ¡Qué punto de referencia genial! En defensa de la expresión regular, para 10 o 100 de miles de campos (lo que la expresión regular está dividiendo) probablemente habría alguna forma de registro (como \nlíneas de texto delimitadas) que comprenda esos campos, por lo que la desaceleración catastrófica probablemente no ocurriría. Si tiene una cadena con 100,000 campos, tal vez Bash no es ideal ;-) Gracias por el punto de referencia. Aprendí una o dos cosas.
dawg
4

Solución de delimitador de caracteres múltiples de bash puro.

Como otros han señalado en este hilo, la pregunta del OP dio un ejemplo de una cadena delimitada por comas para ser analizada en una matriz, pero no indicó si él / ella solo estaba interesado en delimitadores de coma, delimitadores de un solo carácter o de varios caracteres. delimitadores

Dado que Google tiende a clasificar esta respuesta en la parte superior de los resultados de búsqueda o cerca de ella, quería proporcionar a los lectores una respuesta sólida a la pregunta de delimitadores de caracteres múltiples, ya que eso también se menciona en al menos una respuesta.

Si está buscando una solución a un problema de delimitador de caracteres múltiples, le sugiero que revise la publicación de Mallikarjun M , en particular la respuesta de gniourf_gniourf, que proporciona esta elegante solución pura de BASH utilizando la expansión de parámetros:

#!/bin/bash
str="LearnABCtoABCSplitABCaABCString"
delimiter=ABC
s=$str$delimiter
array=();
while [[ $s ]]; do
    array+=( "${s%%"$delimiter"*}" );
    s=${s#*"$delimiter"};
done;
declare -p array

Enlace al comentario citado / publicación referenciada

Enlace a la pregunta citada: ¿ Cómo dividir una cadena en un delimitador de varios caracteres en bash?

Señor Cara de Papa
fuente
1
Vea mi comentario para un enfoque similar pero mejorado.
xebeche
3

Esto funciona para mí en OSX:

string="1 2 3 4 5"
declare -a array=($string)

Si su cadena tiene un delimitador diferente, solo reemplace aquellos con espacio:

string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr "$delimiter" " "))

Simple :-)

A Kra
fuente
Funciona tanto para Bash como para Zsh, ¡lo cual es una ventaja!
Elijah W. Gagne
2

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 apariciones de nuestro delimitador deseado ", "con contenido de $IFSvia "${string//, /$IFS}".

¿Quizás esto sea lento para cadenas muy grandes?

Esto se basa en la respuesta de Dennis Williamson.

Lindsay-necesita-dormir
fuente
2

Encontré esta publicación cuando buscaba analizar una entrada como: word1, word2, ...

nada de lo anterior me ayudó. lo resolvió usando awk. Si ayuda a alguien:

STRING="value1,value2,value3"
array=`echo $STRING | awk -F ',' '{ s = $1; for (i = 2; i <= NF; i++) s = s "\n"$i; print s; }'`
for word in ${array}
do
        echo "This is the word $word"
done
balaganAtomi
fuente
1

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

Geoff Lee
fuente
1

Podemos usar el comando tr para dividir la cadena en el objeto de matriz. Funciona tanto en MacOS como en Linux

  #!/usr/bin/env bash
  currentVersion="1.0.0.140"
  arrayData=($(echo $currentVersion | tr "." "\n"))
  len=${#arrayData[@]}
  for (( i=0; i<=$((len-1)); i++ )); do 
       echo "index $i - value ${arrayData[$i]}"
  done

Otra opción es usar el comando IFS

IFS='.' read -ra arrayData <<< "$currentVersion"
#It is the same as tr
arrayData=($(echo $currentVersion | tr "." "\n"))

#Print the split string
for i in "${arrayData[@]}"
do
    echo $i
done
sopheamak
fuente
0

Utilizar este:

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

#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe
Eduardo Cuomo
fuente
3
Malo: sujeto a división de palabras y expansión de nombre de ruta. Por favor, no revivas viejas preguntas con buenas respuestas para dar malas respuestas.
gniourf_gniourf
2
Esta puede ser una mala respuesta, pero sigue siendo una respuesta válida. Señaladores / revisores: ¡ Para respuestas incorrectas como esta, voto negativo, no elimine!
Scott Weldon
2
@gniourf_gniourf ¿Podría explicar por qué es una mala respuesta? Realmente no entiendo cuando falla.
George Sovetov
3
@GeorgeSovetov: Como dije, está sujeto a la división de palabras y la expansión del nombre de ruta. Más en general, la división una cadena en una matriz como array=( $string )es un (por desgracia muy común) antipatrón: división de palabras se produce: string='Prague, Czech Republic, Europe'; La expansión del nombre de ruta ocurre: string='foo[abcd],bar[efgh]'fallará si tiene un archivo llamado, por ejemplo, foodo barfen su directorio. El único uso válido de tal construcción es cuando stringes un globo.
gniourf_gniourf
0

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

Con un poco menos de ceremonia:

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

p.ej

string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar
usuario1009908
fuente
44
eval es malvado! no hagas esto
caesarsol
1
No. No. Si estás escribiendo guiones lo suficientemente grandes como para que esto importe, lo estás haciendo mal. En el código de la aplicación, eval es malvado. En las secuencias de comandos de shell, es común, necesario e intrascendente.
user1009908
2
ponga un $en su variable y verá ... Escribo muchos guiones y nunca tuve que usar un soloeval
caesarsol
2
Tienes razón, esto solo se puede usar cuando se sabe que la entrada está limpia. No es una solución robusta.
user1009908
La única vez que he tenido que usar eval, fue para una aplicación que se auto generar sus propios códigos / modules ... Y esto nunca tuvo ningún tipo de entrada del usuario ...
enojado 84
0

Aquí está mi truco!

Dividir cadenas por cadenas es algo bastante aburrido de hacer con bash. Lo que sucede es que tenemos enfoques limitados que solo funcionan en algunos casos (divididos por ";", "/", "." Y así sucesivamente) o tenemos una variedad de efectos secundarios en los resultados.

El siguiente enfoque 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.: /dba/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
Eduardo Lucio
fuente
0

Para elementos multilínea, ¿por qué no algo como

$ array=($(echo -e $'a a\nb b' | tr ' ' '§')) && array=("${array[@]//§/ }") && echo "${array[@]/%/ INTERELEMENT}"

a a INTERELEMENT b b INTERELEMENT
Caprichoso
fuente
-1

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
Safter Arslan
fuente
1
Cubro esta idea en mi respuesta ; vea la respuesta incorrecta # 5 (podría estar especialmente interesado en mi discusión del evaltruco). Su solución deja $IFSel valor del espacio de coma después del hecho.
bgoldst
-1

Dado que hay muchas maneras de resolver esto, comencemos definiendo lo que queremos ver en nuestra solución.

  1. Bash proporciona un incorporado readarraypara este propósito. Vamos a usarlo
  2. Evite trucos feos e innecesarios, como cambiar IFS, hacer bucles, usareval o agregar un elemento adicional y luego eliminarlo.
  3. Encuentre un enfoque simple y legible que pueda adaptarse fácilmente a problemas similares.

El readarraycomando es más fácil de usar con líneas nuevas como delimitador. Con otros delimitadores, puede agregar un elemento adicional a la matriz. El enfoque más limpio es primero adaptar nuestra entrada a una forma que funcione bien conreadarray antes de pasarla.

La entrada en este ejemplo no tiene un delimitador de caracteres múltiples. Si aplicamos un poco de sentido común, se entiende mejor como entrada separada por comas para la cual cada elemento puede necesitar ser recortado. Mi solución es dividir la entrada por comas en varias líneas, recortar cada elemento y pasarlo todo readarray.

string='  Paris,France  ,   All of Europe  '
readarray -t foo < <(tr ',' '\n' <<< "$string" |sed 's/^ *//' |sed 's/ *$//')
declare -p foo

# declare -a foo='([0]="Paris" [1]="France" [2]="All of Europe")'
Bryan Roach
fuente
-2

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

rsjethani
fuente
El mismo antipatrón (tristemente común) que otras respuestas: sujeto a división de palabras y expansión de nombre de archivo.
gniourf_gniourf