¿Cómo dividir una cadena en varias cadenas separadas por al menos un espacio en bash shell?

224

Tengo una cadena que contiene muchas palabras con al menos un espacio entre cada dos. ¿Cómo puedo dividir la cadena en palabras individuales para poder recorrerlas?

La cadena se pasa como argumento. Por ej ${2} == "cat cat file". ¿Cómo puedo recorrerlo?

Además, ¿cómo puedo verificar si una cadena contiene espacios?

derrdji
fuente
1
¿Qué tipo de concha? Bash, cmd.exe, powershell ...?
Alexey Sviridov
¿Solo necesita hacer un bucle (por ejemplo, ejecutar un comando para cada una de las palabras)? ¿O necesita almacenar una lista de palabras para su uso posterior?
DVK

Respuestas:

281

¿Intentó simplemente pasar la variable de cadena a un forbucle? Bash, por ejemplo, se dividirá en espacios en blanco automáticamente.

sentence="This is   a sentence."
for word in $sentence
do
    echo $word
done

 

This
is
a
sentence.
multitud
fuente
1
@MobRule: el único inconveniente de esto es que no puede capturar fácilmente (al menos no recuerdo de ninguna manera) la salida para su posterior procesamiento. Ver mi solución "tr" para obtener algo que envía material a STDOUT
DVK
44
Se podía añadir a una variable: A=${A}${word}).
Lucas Jones
1
establecer $ text [esto pondrá las palabras en $ 1, $ 2, $ 3 ... etc.]
Rajesh
32
En realidad, este truco no solo es una solución incorrecta, sino que también es extremadamente peligroso debido a la formación de conchas. touch NOPE; var='* a *'; for a in $var; do echo "[$a]"; donesalidas en [NOPE] [a] [NOPE]lugar de lo esperado [*] [a] [*](LFs reemplazados por SPC para facilitar la lectura).
Tino
@mob ¿qué debo hacer si quiero dividir la cadena en función de alguna cadena específica? ejemplo separador ".xlsx" .
296

Me gusta la conversión a una matriz, para poder acceder a elementos individuales:

sentence="this is a story"
stringarray=($sentence)

ahora puede acceder a elementos individuales directamente (comienza con 0):

echo ${stringarray[0]}

o convertir de nuevo a cadena para hacer un bucle:

for i in "${stringarray[@]}"
do
  :
  # do whatever on $i
done

Por supuesto, el bucle a través de la cadena directamente se respondió antes, pero esa respuesta tenía la desventaja de no realizar un seguimiento de los elementos individuales para su uso posterior:

for i in $sentence
do
  :
  # do whatever on $i
done

Consulte también Referencia de matriz de bash .

Fuerte viento
fuente
26
Lamentablemente, no del todo perfecto, debido al bloqueo de shell: touch NOPE; var='* a *'; arr=($var); set | grep ^arr=salidas en arr=([0]="NOPE" [1]="a" [2]="NOPE")lugar de lo esperadoarr=([0]="*" [1]="a" [2]="*")
Tino
@Tino: si no quieres que interfiera el globbing, simplemente apágalo. La solución funcionará bien con comodines también. Es el mejor enfoque en mi opinión.
Alexandros
3
@Alexandros Mi enfoque es utilizar solo patrones, que son seguros por defecto y funcionan perfectamente en todos los contextos. Un requisito para cambiar el bloqueo de shell para obtener una solución segura es más que un camino muy peligroso, ya es el lado oscuro. Así que mi consejo es que nunca te acostumbres a usar patrones como este aquí, porque tarde o temprano te olvidarás de algunos detalles, y luego alguien explotará tu error. Puede encontrar pruebas de tales hazañas en la prensa. Cada. Soltero. Día.
Tino
86

Solo use los shells "set" incorporados. Por ejemplo,

establecer $ texto

Después de eso, las palabras individuales en $ texto estarán en $ 1, $ 2, $ 3, etc. Para mayor solidez, generalmente se hace

conjunto - basura $ texto
cambio

para manejar el caso donde $ text está vacío o comienza con un guión. Por ejemplo:

text = "Esto es una prueba"
conjunto - basura $ texto
cambio
por palabra; hacer
  echo "[$ palabra]"
hecho

Esto imprime

[Esta]
[es]
[una]
[prueba]
Idelic
fuente
55
Esta es una excelente manera de dividir la var para que se pueda acceder directamente a las partes individuales. +1; resuelto mi problema
Cheekysoft
Iba a sugerir usar awkpero setes mucho más fácil. Ahora soy setfanboy. Gracias @Idelic!
Yzmir Ramirez
22
Tenga en cuenta el bloqueo de shell si hace tales cosas: touch NOPE; var='* a *'; set -- $var; for a; do echo "[$a]"; donesalidas en [NOPE] [a] [NOPE]lugar de las esperadas [*] [a] [*]. ¡Úselo solo si está 101% seguro de que no hay metacaracteres SHELL en la cadena dividida!
Tino
44
@Tino: ese problema se aplica en todas partes, no solo aquí, sino que en este caso podrías set -fantes set -- $vary set +fdespués deshabilitar el globbing.
Ideal
3
@Idelic: Buena captura. Con set -fsu solución también es seguro. Pero set +fes el valor predeterminado de cada shell, por lo que es un detalle esencial, que debe tenerse en cuenta, porque probablemente otros no lo sepan (como yo también).
Tino
81

La forma probablemente más fácil y segura en BASH 3 y superior es:

var="string    to  split"
read -ra arr <<<"$var"

(donde arrestá la matriz que toma las partes divididas de la cadena) o, si puede haber nuevas líneas en la entrada y desea más que solo la primera línea:

var="string    to  split"
read -ra arr -d '' <<<"$var"

(tenga en cuenta el espacio adentro -d '', no se puede dejar de lado), pero esto podría darle una nueva línea inesperada <<<"$var"(ya que esto agrega implícitamente un LF al final).

Ejemplo:

touch NOPE
var="* a  *"
read -ra arr <<<"$var"
for a in "${arr[@]}"; do echo "[$a]"; done

Salidas de lo esperado

[*]
[a]
[*]

ya que esta solución (en contraste con todas las soluciones anteriores aquí) no es propensa a un bloqueo inesperado y, a menudo, incontrolable.

Además, esto le brinda todo el poder de IFS como probablemente desee:

Ejemplo:

IFS=: read -ra arr < <(grep "^$USER:" /etc/passwd)
for a in "${arr[@]}"; do echo "[$a]"; done

Produce algo como:

[tino]
[x]
[1000]
[1000]
[Valentin Hilbig]
[/home/tino]
[/bin/bash]

Como puede ver, los espacios también se pueden preservar de esta manera:

IFS=: read -ra arr <<<' split  :   this    '
for a in "${arr[@]}"; do echo "[$a]"; done

salidas

[ split  ]
[   this    ]

Tenga en cuenta que el manejo de IFSen BASH es un tema en sí mismo, así que haga sus pruebas, algunos temas interesantes sobre esto:

  • unset IFS: Ignora ejecuciones de SPC, TAB, NL y en línea comienza y termina
  • IFS='': Sin separación de campo, solo lee todo
  • IFS=' ': Ejecuciones de SPC (y solo SPC)

Algun ultimo ejemplo

var=$'\n\nthis is\n\n\na test\n\n'
IFS=$'\n' read -ra arr -d '' <<<"$var"
i=0; for a in "${arr[@]}"; do let i++; echo "$i [$a]"; done

salidas

1 [this is]
2 [a test]

mientras

unset IFS
var=$'\n\nthis is\n\n\na test\n\n'
read -ra arr -d '' <<<"$var"
i=0; for a in "${arr[@]}"; do let i++; echo "$i [$a]"; done

salidas

1 [this]
2 [is]
3 [a]
4 [test]

Por cierto:

  • Si no estás acostumbrado $'ANSI-ESCAPED-STRING', es un ahorro de tiempo.

  • Si no incluye -r(como en read -a arr <<<"$var"), la lectura hace que la barra invertida se escape. Esto se deja como ejercicio para el lector.


Para la segunda pregunta:

Para probar algo en una cadena, generalmente me quedo case, ya que esto puede verificar si hay varios casos a la vez (nota: el caso solo ejecuta la primera coincidencia, si necesita fallos, use casedeclaraciones multiplce ), y esta necesidad suele ser el caso (juego de palabras destinado a):

case "$var" in
'')                empty_var;;                # variable is empty
*' '*)             have_space "$var";;        # have SPC
*[[:space:]]*)     have_whitespace "$var";;   # have whitespaces like TAB
*[^-+.,A-Za-z0-9]*) have_nonalnum "$var";;    # non-alphanum-chars found
*[-+.,]*)          have_punctuation "$var";;  # some punctuation chars found
*)                 default_case "$var";;      # if all above does not match
esac

Por lo tanto, puede establecer el valor de retorno para verificar SPC de esta manera:

case "$var" in (*' '*) true;; (*) false;; esac

¿Por qué case? Debido a que generalmente es un poco más legible que las secuencias de expresiones regulares, y gracias a los metacaracteres de Shell, maneja muy bien el 99% de todas las necesidades.

Tino
fuente
2
Esta respuesta merece más votos a favor, debido a las cuestiones destacadas destacadas, y su exhaustividad
Brian Agnew
@brian Gracias. Tenga en cuenta que puede usar set -fo set -o noglobcambiar de globbing, de modo que los metacaracteres de shell ya no causen daño en este contexto. Pero realmente no soy amigo de eso, ya que esto deja mucho poder del shell / es muy propenso a errores para cambiar esta configuración.
Tino
2
Maravillosa respuesta, de hecho merece más votos a favor. Nota al margen sobre la caída del caso: puede usar ;&lograr eso. No estoy seguro de en qué versión de bash apareció. Soy un usuario 4.3
Sergiy Kolodyazhnyy
2
@Serg, gracias por señalar, ya que aún no sabía esto. Así que lo busqué, apareció en Bash4 . ;&es la caída forzada sin verificación de patrones como en C. Y también existe la ;;&que continúa haciendo las verificaciones de patrones adicionales. Así ;;es como if ..; then ..; else if ..y ;;&es como if ..; then ..; fi; if .., donde ;&es como m=false; if ..; then ..; m=:; fi; if $m || ..; then ..: uno nunca deja de aprender (de otros);)
Tino
@Tino Eso es absolutamente cierto: el aprendizaje es un proceso continuo. De hecho, no sabía ;;&antes de que comentaras: D Gracias, y que la concha esté contigo;)
Sergiy Kolodyazhnyy
43
$ echo "This is   a sentence." | tr -s " " "\012"
This
is
a
sentence.

Para verificar espacios, use grep:

$ echo "This is   a sentence." | grep " " > /dev/null
$ echo $?
0
$ echo "Thisisasentence." | grep " " > /dev/null     
$ echo $?
1
DVK
fuente
1
En BASH echo "X" |por lo general se puede sustituir por <<<"X", como esto: grep -s " " <<<"This contains SPC". Puedes ver la diferencia si haces algo como echo X | read varen contraste con read var <<< X. Solo la última importa la variable varal shell actual, mientras que para acceder a ella en la primera variante debe agrupar así:echo X | { read var; handle "$var"; }
Tino
17

(A) Para dividir una oración en sus palabras (separadas por espacios), simplemente puede usar el IFS predeterminado usando

array=( $string )


Ejemplo ejecutando el siguiente fragmento

#!/bin/bash

sentence="this is the \"sentence\"   'you' want to split"
words=( $sentence )

len="${#words[@]}"
echo "words counted: $len"

printf "%s\n" "${words[@]}" ## print array

saldrá

words counted: 8
this
is
the
"sentence"
'you'
want
to
split

Como puede ver, también puede usar comillas simples o dobles sin ningún problema.

Notas:
esto es básicamente lo mismo de respuesta mafia , pero de esta manera almacena la matriz para cualquier otra necesidad. Si solo necesita un solo bucle, puede usar su respuesta, que es una línea más corta :)
: consulte esta pregunta para obtener métodos alternativos para dividir una cadena en función del delimitador.


(B) Para buscar un carácter en una cadena, también puede usar una coincidencia de expresión regular.
Ejemplo para verificar la presencia de un carácter de espacio que puede usar:

regex='\s{1,}'
if [[ "$sentence" =~ $regex ]]
    then
        echo "Space here!";
fi
Luca Borrione
fuente
Para la sugerencia regex (B) a +1, pero -1 para la solución incorrecta (A) ya que es propenso a errores de concha. ;)
Tino
6

Para comprobar espacios solo con bash:

[[ "$str" = "${str% *}" ]] && echo "no spaces" || echo "has spaces"
Glenn Jackman
fuente
1
echo $WORDS | xargs -n1 echo

Esto genera cada palabra, puede procesar esa lista como mejor le parezca después.

Álex
fuente