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 $IFS
variable 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
se 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 $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 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 $string
variable de pasa para contener cualquier LF, a continuación, read
se deje de procesar una vez que encuentre el primer LF. El read
builtin solo procesa una línea por invocación. Esto es cierto incluso si está canalizando o redirigiendo la entrada solo a la read
declaració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 read
incorporado 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 read
construcció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 read
builtin no es apropiado, y debemos evitarlo.
3: Un problema potencial no obvio con esta solución es que 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 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 read
que la división de palabras general también utiliza la $IFS
variable 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 $IFS
caracteres 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 extglob
se 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 -f
antemano 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 ( tr
o sed
), ya que la expansión de parámetros es puramente una operación interna del 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 echo
comando 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 $IFS
y el otro no. Ha resuelto este caso bastante específico eliminando el carácter no representado por IFS utilizando una expansión de sustitución de 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 -f
y 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 $IFS
para contener solo el delimitador de campo de un solo carácter presente en la cadena de entrada. Debe repetirse que esto no puede funcionar 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 -f
y set +f
.
Otro problema potencial es que, dado que LF califica como un "carácter de espacio en blanco IFS" como se definió anteriormente, todos los campos vacíos se perderán, al igual que en # 2 y # 3 . 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 -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 el n . ° 1 (en que se establece $IFS
en 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 eval
llamada 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 eval
de 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 $IFS
solo temporalmente, lo que nos permite evitar todo el gambito de guardar y restaurar como el que se está haciendo con la $OIFS
variable en la primera variante. Pero el desafío que enfrentamos aquí es que el comando que necesitamos ejecutar es en sí mismo una mera asignación de variables, y por lo tanto no implicaría una palabra de comando para hacer que la $IFS
asignació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 : builtin
para hacer que la $IFS
asignación sea temporal? Esto no funciona porque luego también haría que la $array
asignació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 eval
ejecuta 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 $array
asignación dentro del eval
argumento para que tenga efecto en el entorno de shell, mientras que la $IFS
asignación de prefijo que está prefijado al eval
comando no sobrevivirá al eval
comando. 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 $IFS
variable 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 $IFS
variable 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 read
incorporado.
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 $IFS
variable 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 $IFS
que 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 compatxx
configuraciones 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 read
es inapropiado porque realiza dos niveles de división, cuando solo necesitamos uno? El truco aquí es que puede llamar read
de tal manera que efectivamente solo hace un nivel de división, específicamente 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 $IFS
se 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 read
es pasar cero argumentos NAME . En este caso, read
almacenará toda la línea de entrada que obtiene de la secuencia de entrada en una variable denominada $REPLY
y, como beneficio adicional, nosepare los espacios en blanco iniciales y finales del valor. Este es un uso muy robusto read
que 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 -d
opció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 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 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 -d
opció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) read
devolvió el error como (2) $REPLY
está vacío, lo que significa que read
no 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 readarray
construcció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 -O
no 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 read
cuando se analiza el campo, se readarray
elimina 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 -d
opció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 read
solució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 readarray
conservó 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 callback
opció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")
,
(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/…