arrays - into - Dividir cadena en una matriz en Bash
split string by token bash (16)
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 cadena de reemplazo:
${string//substring/replacement}
para reemplazar todas las coincidencias de $ substring con espacio 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 el globo para este script.
En un script de Bash me gustaría dividir una línea en partes 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?
Prueba esto
IFS='', ''; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done
Es sencillo. 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
Todas las respuestas a esta pregunta son incorrectas de una manera u otra.
Respuesta incorrecta # 1
IFS='', '' read -r -a array <<< "$string"
1: Este es un mal uso de $IFS
. El valor de la variable $IFS
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 read
divisiones de la línea de entrada puede terminarse con cualquier carácter en el conjunto (coma o espacio, en este ejemplo).
En realidad, para los verdaderos atacantes, el significado completo de $IFS
es 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 utilizando estos caracteres como terminadores de campo. Si IFS no está configurado, o su valor es exactamente <space><tab> <newline> , el valor predeterminado, luego las secuencias de <space> , <tab> y <newline> al comienzo y al final de los resultados de las expansiones anteriores se ignoran, y cualquier secuencia de caracteres IFS que no esté al principio o al final sirve para delimitar palabras. Si IFS tiene un valor distinto al predeterminado, las secuencias de los caracteres de espacio en blanco <space> , <tab> y <newline> 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 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 produce división de palabras.
Básicamente, para los 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 "caracteres de espacio en blanco de IFS" (es decir, cualquiera de <espacio > , <tab> , y <newline> ("nueva línea" significa línea de alimentación (LF) ) están presentes en cualquier lugar en $IFS
), o (2) cualquier "carácter de espacio en blanco IFS" que esté presente en $IFS
junto con lo que sea " Los caracteres de espacio en blanco de IFS "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 en el espacio u otro equipaje), si el valor de la variable $string
contiene cualquier LF, luego la read
detendrá el procesamiento una vez que encuentre la primera LF. La read
incorporada solo procesa una línea por invocación. Esto es cierto incluso si está canalizando o redireccionando la entrada solo a la declaración de read
, como lo estamos haciendo en este ejemplo con el mecanismo de here-string , y por lo tanto se garantiza que la entrada no procesada se perderá. El código que alimenta la construcción incorporada de read
no tiene conocimiento del flujo de datos dentro de su estructura de comando contenedora.
Podría argumentar que es poco probable que cause un problema, pero aún así, es un peligro sutil que debe evitarse si es posible. Esto se debe al hecho de que la read
incorporada 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 de la construcción incorporada de read
no es apropiado, y debemos evitarlo.
3: Un problema potencial no obvio con esta solución es que la read
siempre 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 al OP no le importe 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 enviarlo a la 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 de 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 que en la read
, la división general de palabras también usa la variable especial $IFS
, aunque en este caso se implica que se establece en su valor predeterminado de <space><tab> <newline> , y por lo tanto, cualquier secuencia de uno o más caracteres IFS (que ahora son todos los 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í sola 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 $IFS
caracteres y, por lo tanto, se dividirían incorrectamente durante la operación de división de palabras. Sucede que 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 usara este idioma correría el riesgo de explotar si esta suposición fue alguna vez violada 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 suele ir seguida de una expansión de nombre de archivo ( también conocida como expansión de nombre de ruta, también " globbing"), que si se hace, corrompería potencialmente las palabras que contienen los caracteres *
,? , o [
seguido de ]
(y, si se establece extglob
, los fragmentos entre paréntesis precedidos por ?
*
, +
, @
, o !
) al hacerlos coincidir con los objetos del sistema de archivos y expandir las palabras ("globos") según corresponda. El primero de estos tres respondedores ha resuelto este problema inteligentemente ejecutando set -f
antemano para deshabilitar 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 subsiguiente 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 expansión de parámetros ${string//:/ }
"sustitución de patrón", en lugar de tener que molestarse en invocar una sustitución de comando (que forse el shell ), iniciando una tubería y ejecutando un ejecutable externo ( tr
o sed
), ya que la expansión de parámetros es puramente una operación interna de shell. (Además, para las soluciones tr
y 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 comando echo
y podría meterse con los valores del campo. Además, el $(...)
la forma de sustitución de comando es preferible a la antigua forma `...`
ya que simplifica el anidamiento de las sustituciones de comando y permite un mejor resaltado de sintaxis por 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 # 2 . La diferencia es que el respondedor ha asumido que los campos están delimitados por dos caracteres, uno de los cuales está representado en el valor predeterminado de $IFS
y el otro no. Él 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 quitarlo y luego depender del carácter de espacio para la división de campos es simplemente incorrecto. Una vez más, considere mi contraejemplo: ''Los Angeles, United States, North America''
.
Además, de nuevo, la expansión del nombre del archivo podría corromper las palabras expandidas, pero esto se puede evitar al deshabilitar temporalmente la creación de espacios para la asignación con set -f
y luego set +f
.
Además, nuevamente, 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 en que usa la división de palabras para hacer el trabajo, solo que ahora el código establece explícitamente $IFS
para que contenga solo el delimitador de campo de un solo carácter presente en la cadena de entrada. Debe repetirse que esto no funciona para los 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 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 envolviendo la declaración crítica en set -f
y set +f
.
Otro problema potencial es que, como 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 que no importe de todos modos, pero vicia la generalidad de la solución.
Entonces, para resumir, suponiendo que tiene un delimitador de un carácter y es un "carácter de espacio en blanco IFS" o no le importan los campos vacíos, y envuelve la declaración crítica en set -f
y 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 # 1 (en que establece $IFS
en coma-espacio) y # 2-4 (en que usa división de palabras para dividir la cadena en campos). Debido a esto, sufre la mayoría de los problemas que afligen a todas las respuestas incorrectas anteriores, como el peor de los mundos.
Además, con respecto a la segunda variante, puede parecer que la llamada eval
es completamente innecesaria, ya que su argumento es un literal de cadena entre comillas simples y, por lo tanto, se conoce estáticamente. Pero en realidad hay un beneficio muy obvio de usar eval
de esta manera. Normalmente, cuando ejecuta un comando simple que consiste solo en una asignación de variable, es decir, sin una palabra de comando real que lo siga, la asignación surte 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 al entorno de shell:
IFS='', '' array=($countries); ## changes both $IFS and $array in the shell environment
Pero, si la asignación de la variable se 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, sin importar si es un componente incorporado. 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 aparece ningún nombre de comando, las asignaciones de variables afectan el entorno de shell actual. De lo contrario, las variables se agregan al entorno del comando ejecutado y no afectan al entorno de shell actual.
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 la variable $OIFS
en la primera variante. Pero el desafío al que nos 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 asignación de $IFS
temporal. Podrías pensar, bueno, ¿por qué no solo agregas una palabra de comando no operable a la declaración como la : builtin
para hacer que la asignación $IFS
temporal? Esto no funciona porque también haría que la asignación $array
temporal también:
IFS='', '' array=($countries) :; ## fails; new $array value never escapes the : command
Por lo tanto, estamos efectivamente en un punto muerto, un poco de catch-22. Pero, cuando eval
ejecuta su código, lo ejecuta en el entorno de shell, como si fuera un código fuente normal, estático, y por lo tanto podemos ejecutar la asignación $array
dentro del argumento eval
para que tenga efecto en el entorno de shell, mientras que la asignación del prefijo $IFS
que está prefijada al comando eval
no sobrevivirá al comando eval
. 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 cumple exactamente lo que se requiere (al menos con respecto a la asignación de la asignación) de una manera bastante poco obvia. En realidad no estoy en contra de este truco en general, a pesar de la participación de eval
; solo tenga cuidado de poner una comilla simple en la cadena del argumento para protegerse contra amenazas de seguridad.
Pero nuevamente, debido a la aglomeración de problemas "del peor de los mundos", esta sigue siendo una respuesta incorrecta al requisito 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 necesita ser analizada en una matriz. Esta "respuesta" comienza con el contenido literal de la cadena de entrada pegada en una matriz literal. Supongo que es una forma de hacerlo.
Parece que la persona que responde puede haber asumido que la variable $IFS
afecta todo el análisis de bash en todos los contextos, lo que no es cierto. Del manual de bash:
IFS El separador de campo interno que se utiliza para dividir palabras después de la expansión y para dividir líneas en palabras con el comando incorporado de lectura . El valor predeterminado es <space><tab> <newline> .
Por lo tanto, la variable especial $IFS
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 líneas de entrada en palabras por la función incorporada de read
.
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, más adelante, 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, no estoy de acuerdo 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 incluso más precisamente, la división de palabras es parte del 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 incluir mucho las palabras "dividir" y "palabras". Aquí hay un extracto relevante de la versión de linux.die.net del manual de bash:
La expansión se realiza en la línea de comandos después de que se ha dividido en palabras. Se realizan siete tipos de expansión: expansión de corsé , expansión de tilde , expansión de parámetros y variables , sustitución de comandos , expansión aritmética , división de palabras y expansión de nombre de ruta .
El orden de las expansiones es: expansión corsé; expansión de tilde, expansión de parámetros y variables, expansión aritmética y sustitución de comandos (realizada de izquierda a derecha); división de palabras y la expansión del nombre de ruta.
Podría argumentar que la versión GNU del manual funciona ligeramente 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 comandos después de que se haya dividido en tokens.
El punto importante es que $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 de shell, como las secuencias de comandos, las listas de comandos, las tuberías, las expansiones de parámetros, las sustituciones aritméticas y las sustituciones de comandos. En su mayor parte, el proceso de análisis de bash no se puede alterar mediante acciones a nivel de usuario como asignaciones de variables (en realidad, hay algunas excepciones menores a esta regla; por ejemplo, vea las diversas configuraciones de shell de compatxx
, que pueden cambiar ciertos aspectos del comportamiento de análisis sobre la marcha). Las "palabras" / "símbolos" en sentido ascendente 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 la documentación anterior, donde la división de palabras del texto expandido (¿en expansión?) Las palabras son 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 desde la fuente bytestream.
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 estamos de nuevo a utilizar 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 a la read
de tal manera que efectivamente solo haga 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 juego de manos, pero funciona.
Pero hay problemas. Primero: cuando proporciona al menos un argumento NAME para read
, automáticamente ignora los espacios en blanco iniciales y finales en cada campo que se separa de la cadena de entrada. Esto ocurre si $IFS
se establece en su valor predeterminado o no, como se describió anteriormente en esta publicación. Ahora, el OP puede no preocuparse por este 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. Sin embargo, hay una solución: un uso un tanto no obvio de la read
es pasar cero argumentos NAME . En este caso, read
almacenará la línea de entrada completa que recibe del flujo de entrada en una variable llamada $REPLY
, y, como bonificación, no elimina el espacio en blanco inicial y final del valor. Este es un uso muy robusto de la read
que he explotado con frecuencia en mi carrera de programación en shell. Aquí hay 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 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 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 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 la read
devuelve 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 perdemos el campo final.
Técnicamente, este mismo error afectó también a los ejemplos anteriores; la diferencia es que se tomó el separador de campo como 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 que se encuentra antes de que lo alimente como entrada al comando. Por lo tanto, en esos casos, resolvimos accidentalmente el problema de un campo final abandonado agregando involuntariamente un terminador ficticio adicional a la entrada. Llamemos a esta solución la solución "terminante ficticio". Podemos aplicar la solución de terminación de simulacros manualmente para cualquier delimitador personalizado concatenándola con la cadena de entrada nosotros mismos al crear una instancia de la siguiente cadena:
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")
Allí, el problema resuelto. Otra solución es solo romper el ciclo while si tanto (1) read
devolvió el error como (2) $REPLY
está vacío, lo que significa que la read
no pudo leer ningún carácter antes de llegar al 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 se agrega automáticamente a la cadena de aquí por el operador de redirección <<<
. 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 simulacros de resolución lo resuelve directamente, por lo que podemos continuar con eso. La solución manual de terminación de simulacros es en realidad bastante conveniente, ya que resuelve estos dos problemas (el problema de campo final descartado y el problema de LF anexado) de una sola vez.
Entonces, en general, esta es una solución bastante poderosa. Solo queda la debilidad por la falta de soporte para 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 de la misma publicación que # 7 ; el respondedor proporcionó dos soluciones en la misma publicación).
El readarray
, que es un sinónimo de mapfile
, es ideal. Es un comando incorporado que analiza un bytestream en una variable de matriz en un disparo; no se mete con los bucles, condicionales, sustituciones o cualquier otra cosa. Y no elimina de manera subrepticia ningún espacio en blanco de la cadena de entrada. Y (si no se da -O
), convenientemente borra la matriz de destino antes de asignársela. Pero todavía no es perfecto, de ahí mi crítica como una "respuesta incorrecta".
En primer lugar, solo para eliminar esto, tenga en cuenta que, al igual que el comportamiento de la read
cuando se realiza el análisis de campo, readarray
elimina el campo final si está vacío. Nuevamente, esto probablemente no sea 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 admite delimitadores de caracteres múltiples. Voy a dar una solución para esto en un momento también.
En tercer lugar, la solución como está escrita no analiza la cadena de entrada del OP, y de hecho, no se puede usar como está para analizarla. También ampliaré esto momentáneamente.
Por las razones anteriores, sigo considerando que esta es una "respuesta incorrecta" a la pregunta del OP. A continuación daré lo que considero la respuesta correcta.
Respuesta correcta
Este es un intento ingenuo de hacer que el trabajo # 8 solo especifique 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 read
bucle que se analiza en # 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 readarray
conservó el campo final, ya que el operador de redirección <<<
adjuntó el LF a la cadena de entrada y, por lo tanto, el campo posterior no estaba vacío (de lo contrario, se habría eliminado). Podemos encargarnos de esto al anular explícitamente el elemento final de la matriz 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 los delimitadores de caracteres múltiples.
El espacio en blanco podría, por supuesto, recortarse después (por ejemplo, consulte ¿Cómo recortar espacios en blanco desde una variable Bash? ). Pero si podemos hackear un delimitador de caracteres múltiples, entonces 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 garantiza que no colisionará 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 hacer en línea en una sustitución de proceso. Aquí está 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")
¡Ahí, por fin! Esta solución no dividirá erróneamente los campos en el medio, no se cortará prematuramente, no se eliminarán los campos vacíos, no se corromperá a sí misma en las expansiones de nombres de archivos, 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 recorte
Por último, quería demostrar mi propia solución de recorte bastante intrincada utilizando la opción de -C callback
de -C callback
oscura -C callback
de readarray
. Desafortunadamente, me he quedado sin espacio frente al draconiano de 30.000 caracteres de , 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")
Utilizar esta:
countries=''Paris, France, Europe''
OIFS="$IFS"
IFS='', '' array=($countries)
IFS="$OIFS"
#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe
¡Aquí está mi truco!
Dividir cadenas por cadenas es una cosa bastante aburrida de hacer usando 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 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
Pura solución de delimitador multi-carácter bash.
Como otros lo 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 comas, delimitadores de caracteres individuales o caracteres múltiples delimitadores
Dado que Google tiende a clasificar esta respuesta en o cerca de la parte superior de los resultados de búsqueda, quise ofrecer a los lectores una respuesta sólida a la pregunta de los delimitadores de caracteres múltiples, ya que eso también se menciona en al menos una respuesta.
Si estás buscando una solución a un problema de delimitadores de múltiples caracteres, sugiero revisar la publicación de Mallikarjun M , en particular la respuesta de gniourf_gniourf que proporciona esta elegante solución BASH pura utilizando la expansión de parámetros:
str="a, b, c, d" # assuming there is a space after '','' as in Q
arr=(${str//,/}) # delete all occurrences of '',''
Enlace al comentario citado / publicación referenciada
Enlace a la pregunta citada: ¿ Cómo dividir una cadena en un delimitador de múltiples caracteres en bash?
ACTUALIZACIÓN: No hagas 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
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
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 el contenido de $IFS
vía "${string//, /$IFS}"
.
Tal vez esto sea lento para cuerdas muy grandes aunque?
Esto se basa en la respuesta de Dennis Williamson.
Otra forma sería:
for i in ${arr[@]}; do echo $i; done
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
Otro enfoque puede ser:
string="Paris, France, Europe"
IFS='', '' arr=(${string})
Después de este ''arr'' es una matriz con cuatro cadenas. Esto no requiere tratar con IFS o leer o cualquier otra cosa especial, por lo tanto mucho más simple y directo.
A veces me ocurrió 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
La clave para dividir la cadena en una matriz es el delimitador de múltiples caracteres de ", "
. Cualquier solución que use IFS
para delimitadores de múltiples caracteres es inherentemente incorrecta, ya que IFS es un conjunto de esos caracteres, no una cadena.
Si lo asigna IFS=", "
, la cadena se romperá en CUALQUIER ","
O en " "
cualquier combinación de ellas que no sea una representación precisa del delimitador de dos caracteres ", "
.
Puede usar awk
o sed
para 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.
Editado por bgoldst: Aquí hay algunos puntos de referencia que comparan mi readarray
solución con la solución de expresiones regulares de dawg, y también incluí la read
solución para cada detalle (nota: modifiqué ligeramente la solución de expresiones regulares para lograr una mayor armonía con mi solución) (también vea mis comentarios debajo de 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
##
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 más simple bash readarray :
readarray -t lines <<<"$string"
Imprimir todas las líneas es muy fácil aprovechando una característica de printf:
printf ">[%s]/n" "${lines[@]}"
>[first line]
>[ second line]
>[ third line]
IFS='', '' read -r -a array <<< "$string"
Tenga en cuenta que los caracteres en $IFS
se tratan individualmente como separadores, de modo que en este caso los campos pueden estar separados por una coma o un espacio en lugar de la secuencia de los dos caracteres. Sin embargo, resulta interesante que 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 arreglos 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 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 en Bash 4.2 y más tarde:
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 el formulario más antiguo. Es requerido.
t="one,two,three"
a=($(echo "$t" | tr '','' ''/n''))
echo "${a[2]}"
Imprime tres