Comment diviser une chaîne en plusieurs chaînes séparées par au moins un espace dans le shell bash?


224

J'ai une chaîne contenant de nombreux mots avec au moins un espace entre les deux. Comment puis-je diviser la chaîne en mots individuels afin de pouvoir les parcourir?

La chaîne est passée en argument. Par exemple ${2} == "cat cat file". Comment puis-je le parcourir?

De plus, comment puis-je vérifier si une chaîne contient des espaces?


1
Quel genre de coquille? Bash, cmd.exe, PowerShell ...?
Alexey Sviridov, le

Avez-vous juste besoin de boucler (par exemple, exécuter une commande pour chacun des mots)? Ou avez-vous besoin de stocker une liste de mots pour une utilisation ultérieure?
DVK

Réponses:


281

Avez-vous essayé de simplement passer la variable chaîne à une forboucle? Bash, pour sa part, se divisera automatiquement sur les espaces.

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

 

This
is
a
sentence.

1
@MobRule - le seul inconvénient de ceci est que vous ne pouvez pas facilement capturer (du moins je ne me souviens pas d'un moyen) la sortie pour un traitement ultérieur. Voir ma solution "tr" ci-dessous pour quelque chose qui envoie des trucs à STDOUT
DVK

4
Vous pouvez simplement ajouter à une variable: A=${A}${word}).
Lucas Jones

1
set $ text [cela mettra les mots dans $ 1, $ 2, $ 3 ... etc]
Rajesh

32
En fait, cette astuce n'est pas seulement une mauvaise solution, elle est également extrêmement dangereuse en raison de l'éclatement de la coque. touch NOPE; var='* a *'; for a in $var; do echo "[$a]"; donesorties [NOPE] [a] [NOPE]au lieu des attentes [*] [a] [*](LF remplacés par SPC pour plus de lisibilité).
Tino

@mob que dois-je faire si je souhaite diviser la chaîne en fonction d'une chaîne spécifique? exemple séparateur ".xlsx" .

296

J'aime la conversion en tableau, pour pouvoir accéder aux éléments individuels:

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

vous pouvez maintenant accéder directement aux éléments individuels (il commence par 0):

echo ${stringarray[0]}

ou reconvertissez en chaîne pour boucler:

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

Bien sûr, le fait de boucler directement dans la chaîne a été répondu auparavant, mais cette réponse avait l'inconvénient de ne pas suivre les éléments individuels pour une utilisation ultérieure:

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

Voir aussi Bash Array Reference .


26
Malheureusement pas tout à fait parfait, à cause du shell-globbing: touch NOPE; var='* a *'; arr=($var); set | grep ^arr=sorties arr=([0]="NOPE" [1]="a" [2]="NOPE")au lieu des attentesarr=([0]="*" [1]="a" [2]="*")
Tino

@Tino: si vous ne voulez pas que le globbing interfère, désactivez-le simplement. La solution fonctionnera également correctement avec les caractères génériques. C'est la meilleure approche à mon avis.
Alexandros

3
@Alexandros Mon approche est de n'utiliser que des modèles, qui sont sécurisés par défaut et qui fonctionnent parfaitement dans tous les contextes. Une exigence de changer la globalisation des coquilles pour obtenir une solution sécurisée est plus qu'un simple chemin très dangereux, c'est déjà le côté obscur. Donc, mon conseil est de ne jamais vous habituer à utiliser un modèle comme celui-ci ici, car tôt ou tard, vous oublierez certains détails, puis quelqu'un exploitera votre bogue. Vous pouvez trouver la preuve de tels exploits dans la presse. Chaque. Célibataire. Journée.
Tino

86

Utilisez simplement les coques "set" intégrées. Par exemple,

définir $ text

Après cela, les mots individuels dans $ text seront dans $ 1, $ 2, $ 3, etc. Pour la robustesse, on fait généralement

set - junk $ text
décalage

pour gérer le cas où $ text est vide ou commencez par un tiret. Par exemple:

text = "Ceci est un test"
set - junk $ text
décalage
pour mot; faire
  echo "[$ word]"
terminé

Ceci imprime

[Ce]
[est]
[une]
[tester]

5
Il s'agit d'un excellent moyen de diviser la var afin d'accéder directement aux pièces individuelles. +1; résolu mon problème
Cheekysoft

J'allais suggérer d'utiliser awkmais setc'est beaucoup plus facile. Je suis maintenant setfanboy. Merci @Idelic!
Yzmir Ramirez

22
Veuillez être conscient du shell globbing si vous faites de telles choses: touch NOPE; var='* a *'; set -- $var; for a; do echo "[$a]"; donesorties [NOPE] [a] [NOPE]au lieu de ce qui est attendu [*] [a] [*]. Utilisez-le uniquement si vous êtes sûr à 101% qu'il n'y a pas de métacaractères SHELL dans la chaîne fractionnée!
Tino

4
@Tino: Ce problème s'applique partout, non seulement ici, mais dans ce cas, vous pouvez juste set -favant set -- $varet set +faprès désactiver la globalisation.
Idelic

3
@Idelic: Bonne prise. Avec set -fvotre solution est sûr, aussi. Mais set +fc'est la valeur par défaut de chaque shell, c'est donc un détail essentiel, qui doit être noté, car d'autres ne le savent probablement pas (comme moi aussi).
Tino

81

Le moyen probablement le plus simple et le plus sûr dans BASH 3 et supérieur est:

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

(où arrest le tableau qui prend les parties fractionnées de la chaîne) ou, s'il peut y avoir des sauts de ligne dans l'entrée et que vous voulez plus que la première ligne:

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

(veuillez noter l'espace dans -d '', il ne peut pas être laissé de côté), mais cela pourrait vous donner une nouvelle ligne inattendue de <<<"$var"(car cela ajoute implicitement un LF à la fin).

Exemple:

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

Produit les résultats attendus

[*]
[a]
[*]

car cette solution (contrairement à toutes les solutions précédentes ici) n'est pas sujette à un globbing shell inattendu et souvent incontrôlable.

Cela vous donne également la pleine puissance d'IFS comme vous le souhaitez probablement:

Exemple:

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

Produit quelque chose comme:

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

Comme vous pouvez le voir, les espaces peuvent également être préservés de cette façon:

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

les sorties

[ split  ]
[   this    ]

Veuillez noter que la gestion de IFSBASH est un sujet en soi, alors faites vos tests, quelques sujets intéressants à ce sujet:

  • unset IFS: Ignore les exécutions de SPC, TAB, NL et commence et se termine en ligne
  • IFS='': Pas de séparation de champ, lit tout
  • IFS=' ': Exécutions de SPC (et SPC uniquement)

Un dernier exemple

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

les sorties

1 [this is]
2 [a test]

tandis que

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

les sorties

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

BTW:

  • Si vous n'avez pas l'habitude de $'ANSI-ESCAPED-STRING'vous y habituer, c'est un gain de temps.

  • Si vous n'incluez pas -r(comme dans read -a arr <<<"$var"), alors read n'échappe pas à la barre oblique inverse. Ceci est laissé comme exercice pour le lecteur.


Pour la deuxième question:

Pour tester quelque chose dans une chaîne, je m'en tiens habituellement case, car cela peut vérifier plusieurs cas à la fois (remarque: la casse n'exécute que la première correspondance, si vous avez besoin d'utiliser des caseinstructions de multiplication ), et ce besoin est assez souvent le cas (pun prévu):

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

Vous pouvez donc définir la valeur de retour pour vérifier le SPC comme ceci:

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

Pourquoi case? Parce qu'il est généralement un peu plus lisible que les séquences d'expression régulière, et grâce aux métacaractères Shell, il gère très bien 99% de tous les besoins.


2
Cette réponse mérite plus de votes positifs, en raison des problèmes de globalisation mis en évidence et de son exhaustivité
Brian Agnew

@brian Merci. Veuillez noter que vous pouvez utiliser set -fou set -o noglobchanger de globbing, de sorte que les métacaractères du shell ne fassent plus de mal dans ce contexte. Mais je ne suis pas vraiment un ami de cela, car cela laisse beaucoup de puissance de la coque / est très enclin à commuter dans les deux sens ce paramètre.
Tino

2
Merveilleuse réponse, mérite en effet plus de votes positifs. Note complémentaire sur la chute du boîtier - vous pouvez l'utiliser pour y ;&parvenir. Je ne sais pas trop dans quelle version de bash est apparue. Je suis un utilisateur 4.3
Sergiy Kolodyazhnyy

2
@Serg merci de l'avoir noté, car je ne le savais pas encore! Alors je l'ai recherché, il est apparu dans Bash4 . ;&est la chute forcée sans vérification de modèle comme en C. Et il y a aussi ;;&qui continue juste à faire les vérifications de modèle supplémentaires. Il en ;;est de même if ..; then ..; else if ..et ;;&est comme if ..; then ..; fi; if .., où ;&est comme m=false; if ..; then ..; m=:; fi; if $m || ..; then ..- on n'arrête jamais d'apprendre (des autres);)
Tino

@Tino C'est absolument vrai - l'apprentissage est un processus continu. En fait, je ne le savais pas ;;&avant que vous commentiez: D Merci, et que le coquillage soit avec vous;)
Sergiy Kolodyazhnyy

43
$ echo "This is   a sentence." | tr -s " " "\012"
This
is
a
sentence.

Pour vérifier les espaces, utilisez grep:

$ echo "This is   a sentence." | grep " " > /dev/null
$ echo $?
0
$ echo "Thisisasentence." | grep " " > /dev/null     
$ echo $?
1

1
Dans BASH echo "X" |peut généralement être remplacé par <<<"X", comme ceci: grep -s " " <<<"This contains SPC". Vous pouvez voir la différence si vous faites quelque chose comme echo X | read varcontrairement à read var <<< X. Seul ce dernier importe la variable vardans le shell courant, alors pour y accéder dans la première variante il faut grouper comme ceci:echo X | { read var; handle "$var"; }
Tino

17

(A) Pour diviser une phrase en ses mots (séparés par des espaces), vous pouvez simplement utiliser l'IFS par défaut en utilisant

array=( $string )


Exemple d' exécution de l'extrait de code suivant

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

affichera

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

Comme vous pouvez le voir, vous pouvez également utiliser des guillemets simples ou doubles sans aucun problème

Remarques:
- c'est fondamentalement la même chose que la réponse de mob , mais de cette façon, vous stockez le tableau pour tout autre besoin. Si vous n'avez besoin que d'une seule boucle, vous pouvez utiliser sa réponse, qui est d'une ligne plus courte :)
- veuillez vous référer à cette question pour d'autres méthodes pour diviser une chaîne en fonction du délimiteur.


(B) Pour rechercher un caractère dans une chaîne, vous pouvez également utiliser une correspondance d'expression régulière.
Exemple pour vérifier la présence d'un caractère espace que vous pouvez utiliser:

regex='\s{1,}'
if [[ "$sentence" =~ $regex ]]
    then
        echo "Space here!";
fi

Pour l'indice regex (B) un +1, mais -1 pour une mauvaise solution (A) car c'est une erreur sujette à l'écrasement du shell. ;)
Tino

6

Pour vérifier les espaces uniquement avec bash:

[[ "$str" = "${str% *}" ]] && echo "no spaces" || echo "has spaces"

1
echo $WORDS | xargs -n1 echo

Cela génère chaque mot, vous pouvez traiter cette liste comme bon vous semble par la suite.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.