Bash: dividir cadena en matriz de caracteres

Tengo una cadena en un script de shell Bash que quiero dividir en una matriz de caracteres, no basada en un delimitador, sino solo un carácter por índice de matriz. ¿Cómo puedo hacer esto? Idealmente, no usaría ningún progtwig externo. Déjame reformular eso. Mi objective es la portabilidad, por lo que cosas como sed que probablemente estén en cualquier sistema compatible con POSIX están bien.

Tratar

 echo "abcdefg" | fold -w1 

Editar: se agregó una solución más elegante sugerida en los comentarios.

 echo "abcdefg" | grep -o . 

Puede acceder a cada letra individualmente sin una conversión de matriz:

 $ foo="bar" $ echo ${foo:0:1} b $ echo ${foo:1:1} a $ echo ${foo:2:1} r 

Si eso no es suficiente, podrías usar algo como esto:

 $ bar=($(echo $foo|sed 's/\(.\)/\1 /g')) $ echo ${bar[1]} a 

Si no puedes usar sed o algo así, puedes usar la primera técnica anterior combinada con un ciclo while usando la longitud de la cadena original ( ${#foo} ) para construir la matriz.

Advertencia: el código siguiente no funciona si la cadena contiene espacios en blanco. Creo que la respuesta de Vaughn Cato tiene más posibilidades de sobrevivir con caracteres especiales.

 thing=($(i=0; while [ $i -lt ${#foo} ] ; do echo ${foo:$i:1} ; i=$((i+1)) ; done)) 

Si su cadena está almacenada en la variable x, esto produce una matriz y con los caracteres individuales:

 i=0 while [ $i -lt ${#x} ]; do y[$i]=${x:$i:1}; i=$((i+1));done 

Como alternativa a iterar sobre 0 .. ${#string}-1 con un ciclo for / while, hay otras dos maneras en que se me ocurre hacer esto solo con bash : using =~ y usando printf . (Hay una tercera posibilidad usando eval y una expresión de secuencia {..} , pero esto carece de claridad.)

Con el entorno correcto y el NLS habilitado en bash, estos funcionarán con un ASCII como se esperaba, eliminando posibles fonts de falla con herramientas del sistema más antiguas como sed , si eso es una preocupación. Estos funcionarán desde bash-3.0 (lanzado en 2005).

Usando =~ y expresiones regulares, convirtiendo una cadena en una matriz en una sola expresión:

 string="wonkabars" [[ "$string" =~ ${string//?/(.)} ]] # splits into array printf "%s\n" "${BASH_REMATCH[@]:1}" # loop free: reuse fmtstr declare -a arr=( "${BASH_REMATCH[@]:1}" ) # copy array for later 

La forma en que esto funciona es realizar una expansión de string que sustituye a cada carácter por (.) , Luego hacer coincidir esta expresión regular generada con la agrupación para capturar cada carácter individual en BASH_REMATCH[] . El índice 0 se establece en la cadena completa, ya que esa matriz especial es de solo lectura, no se puede eliminar, tenga en cuenta :1 cuando la matriz se expanda para omitir el índice 0, si es necesario. Algunas pruebas rápidas para cadenas no triviales (> 64 caracteres) muestran que este método es sustancialmente más rápido que uno que usa cadenas de bash y operaciones de matriz.

Lo anterior funcionará con cadenas que contengan líneas nuevas, =~ admite POSIX ERE donde . coincide con cualquier cosa excepto NUL por defecto, es decir, la expresión regular se comstack sin REG_NEWLINE . (El comportamiento de las utilidades de procesamiento de texto POSIX puede ser diferente por defecto a este respecto, y generalmente lo es).

Segunda opción, usando printf :

 string="wonkabars" ii=0 while printf "%s%n" "${string:ii++:1}" xx; do ((xx)) && printf "\n" || break done 

Este ciclo incrementa el índice ii para imprimir un carácter a la vez, y se activa cuando no quedan caracteres. Esto sería incluso más simple si el bash printf devolvió el número de caracteres impresos (como en C) en lugar de un estado de error, en cambio el número de caracteres impresos se captura en xx usando %n . (Esto funciona al menos en cuanto a bash-2.05b).

Con bash-3.1 y printf -v var tiene un poco más de flexibilidad, y puede evitar caerse al final de la cadena en caso de que esté haciendo algo más que imprimir los caracteres, por ejemplo, para crear una matriz:

 declare -a arr ii=0 while printf -v cc "%s%n" "${string:(ii++):1}" xx; do ((xx)) && arr+=("$cc") || break done 

La solución más simple, completa y elegante:

 $ read -a ARRAY <<< $(echo "abcdefg" | sed 's/./& /g') 

y prueba

 $ echo ${ARRAY[0]} a $ echo ${ARRAY[1]} b 

Explicación : read -a lee stdin como una matriz y lo asigna a la variable ARRAY tratando los espacios como delimitadores para cada elemento de la matriz.

La evaluación de hacer eco de la cadena a sed solo agrega espacios necesarios entre cada personaje.

Estamos usando Here String (<<<) para alimentar el comando stdin del comando de lectura.

Si el texto puede contener espacios:

 eval a=( $(echo "this is a test" | sed "s/\(.\)/'\1' /g") ) 
 $ echo hello | awk NF=NF FS= hello 

O

 $ echo hello | awk '$0=RT' RS=[[:alnum:]] h e l l o 
 string=hello123 for i in $(seq 0 ${#string}) do array[$i]=${string:$i:1} done echo "zero element of array is [${array[0]}]" echo "entire array is [${array[@]}]" 

El elemento cero de la matriz es [h] . Toda la matriz es [hello 1 2 3 ] .

Si desea almacenar esto en una matriz, puede hacer esto:

 string=foo unset chars declare -a chars while read -N 1 do chars[${#chars[@]}]="$REPLY" done <<<"$string"x unset chars[$((${#chars[@]} - 1))] unset chars[$((${#chars[@]} - 1))] echo "Array: ${chars[@]}" Array: foo echo "Array length: ${#chars[@]}" Array length: 3 

La x final es necesaria para manejar el hecho de que se agrega una nueva línea después de $string si no contiene una.

Si desea usar caracteres separados por NUL, puede intentar esto:

 echo -n "$string" | while read -N 1 do printf %s "$REPLY" printf '\0' done 

AWK es bastante conveniente:

 a='123'; echo $a | awk 'BEGIN{FS="";OFS=" "} {print $1,$2,$3}' 

donde FS y OFS son delimitadores para lectura e impresión

Para aquellos que aterrizaron aquí buscando cómo hacer esto en el pescado :

Podemos usar el comando de string incorporada (desde v2.3.0) para la manipulación de cadenas.

 ↪ string split '' abc a b c 

El resultado es una lista, por lo que las operaciones de matriz funcionarán.

 ↪ for c in (string split '' abc) echo char is $c end char is a char is b char is c 

Aquí hay un ejemplo más complejo iterando sobre la cadena con un índice.

 ↪ set --local chars (string split '' abc) for i in (seq (count $chars)) echo $i: $chars[$i] end 1: a 2: b 3: c 

Como respuesta a Alexandro de Oliveira, creo que lo siguiente es más elegante o al menos más intuitivo:

 while read -r -n1 c ; do arr+=("$c") ; done <<<"hejsan"