134 votes

Comment puis-je échapper aux espaces blancs dans une liste de boucles bash ?

J'ai un shell bash script qui boucle sur tous les répertoires enfants (mais pas les fichiers) d'un certain répertoire. Le problème est que certains des noms de répertoire contiennent des espaces.

Voici le contenu de mon répertoire de test :

$ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

Et le code qui boucle sur les répertoires :

for f in `find test/* -type d`; do
  echo $f
done

Voici le résultat :

test/Baltimore
test/Cherry
Hill
test/Edison 
test/New
York
City
test/Philadelphia

Cherry Hill et New York City sont traités comme 2 ou 3 entrées distinctes.

J'ai essayé de citer les noms de fichiers, comme ceci :

for f in `find test/* -type d | sed -e 's/^/\"/' | sed -e 's/$/\"/'`; do
  echo $f
done

mais en vain.

Il doit y avoir un moyen simple de faire ça.


Les réponses ci-dessous sont excellentes. Mais pour rendre les choses plus compliquées, je ne veux pas toujours utiliser les répertoires listés dans mon répertoire de test. Parfois, je veux passer les noms des répertoires en tant que paramètres de ligne de commande.

J'ai suivi la suggestion de Charles de régler l'IFS et j'ai obtenu les résultats suivants :

dirlist="${@}"
(
  [[ -z "$dirlist" ]] && dirlist=`find test -mindepth 1 -type d` && IFS=$'\n'
  for d in $dirlist; do
    echo $d
  done
)

et cela fonctionne parfaitement, sauf s'il y a des espaces dans les arguments de la ligne de commande (même si ces arguments sont cités). Par exemple, appeler le script comme ceci : test.sh "Cherry Hill" "New York City" produit la sortie suivante :

Cherry
Hill
New
York
City

0 votes

Re : edit, list="$@" élimine complètement le caractère de liste de la valeur originale, la réduisant à une chaîne de caractères. Veuillez suivre les pratiques décrites dans ma réponse exactement comme indiqué -- Si vous voulez passer une liste d'arguments de ligne de commande à un programme, vous devez les rassembler dans un tableau, et développer ce tableau directement.

111voto

Charles Duffy Points 34134

D'abord, ne le faites pas de cette façon. La meilleure approche est d'utiliser find -exec correctement :

# this is safe
find test -type d -exec echo '{}' +

L'autre approche sûre est d'utiliser des listes terminées par NUL, bien que cela nécessite que votre find supporte -print0 :

# this is safe
while IFS= read -r -d '' n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d -print0)

Vous pouvez également remplir un tableau à partir de find, et passer ce tableau plus tard :

# this is safe
declare -a myarray
while IFS= read -r -d '' n; do
  myarray+=( "$n" )
done < <(find test -mindepth 1 -type d -print0)
printf '%q\n' "${myarray[@]}" # printf is an example; use it however you want

Si votre découverte ne supporte pas -print0 votre résultat n'est pas sûr - le fichier ci-dessous ne se comportera pas comme souhaité s'il existe des fichiers contenant des nouvelles lignes dans leurs noms (ce qui, oui, est légal) :

# this is unsafe
while IFS= read -r n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d)

Si l'on n'utilise pas l'une des méthodes ci-dessus, une troisième approche (moins efficace en termes de temps et d'utilisation de la mémoire, car elle lit l'intégralité de la sortie du sous-processus avant de procéder au découpage des mots) consiste à utiliser un fichier de type IFS qui ne contient pas le caractère espace. Désactiver le globbing ( set -f ) pour empêcher les chaînes contenant des caractères globaux tels que [] , * o ? de s'étendre :

# this is unsafe (but less unsafe than it would be without the following precautions)
(
 IFS=$'\n' # split only on newlines
 set -f    # disable globbing
 for n in $(find test -mindepth 1 -type d); do
   printf '%q\n' "$n"
 done
)

Enfin, pour les paramètres de ligne de commande, vous devriez utiliser des tableaux si votre shell les prend en charge (c'est-à-dire s'il s'agit de ksh, bash ou zsh) :

# this is safe
for d in "$@"; do
  printf '%s\n' "$d"
done

maintiendra la séparation. Notez que la citation (et l'utilisation de la touche $@ plutôt que $* ) est important. Les tableaux peuvent également être remplis d'autres manières, comme les expressions globales :

# this is safe
entries=( test/* )
for d in "${entries[@]}"; do
  printf '%s\n' "$d"
done

1 votes

Je ne savais pas qu'il y avait un "+" pour -exec. Cool.

1 votes

On dirait qu'il peut aussi, comme xargs, ne mettre les arguments qu'à la fin de la commande donnée :/ cela m'a parfois gêné

0 votes

Je pense que -exec [nom] {} + est une extension GNU et 4.4-BSD. (En tout cas, il n'apparaît pas sur Solaris 8, et je ne pense pas qu'il était dans AIX 4.3 non plus). Je suppose que le reste d'entre nous doit se contenter de passer par xargs...

28voto

find . -type d | while read file; do echo $file; done

Cependant, cela ne fonctionne pas si le nom du fichier contient des nouvelles lignes. La solution ci-dessus est la seule que je connaisse lorsque vous voulez réellement avoir le nom du répertoire dans une variable. Si vous voulez simplement exécuter une commande, utilisez xargs.

find . -type d -print0 | xargs -0 echo 'The directory is: '

0 votes

Pas besoin de xargs, voir find -exec ... {} +

4 votes

@Charles : pour un grand nombre de fichiers, xargs est beaucoup plus efficace : il ne génère qu'un seul processus. L'option -exec crée un nouveau processus pour chaque fichier, ce qui peut être un ordre de grandeur plus lent.

1 votes

J'aime mieux xargs. Ces deux-là semblent essentiellement faire la même chose, alors que xargs a plus d'options, comme l'exécution en parallèle

24voto

cbliard Points 1176

Voici une solution simple qui gère les tabulations et/ou les espaces dans le nom du fichier. Si vous devez gérer d'autres caractères étranges dans le nom de fichier, comme les nouvelles lignes, choisissez une autre solution.

Le répertoire de test

ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

Le code à placer dans les répertoires

find test -type d | while read f ; do
  echo "$f"
done

Le nom du fichier doit être cité ( "$f" ) si elle est utilisée comme argument. Sans les guillemets, les espaces agissent comme séparateur d'argument et des arguments multiples sont donnés à la commande invoquée.

Et le résultat :

test/Baltimore
test/Cherry Hill
test/Edison
test/New York City
test/Philadelphia

0 votes

Merci, cela a fonctionné pour l'alias que je créais pour lister l'espace utilisé par chaque répertoire du dossier courant, il s'étranglait sur certains répertoires avec des espaces dans l'incarnation précédente. Cela fonctionne avec zsh, mais certaines des autres réponses ne le faisaient pas : alias duc='ls -d * | while read D; do du -sh "$D"; done;'

2 votes

Si vous utilisez zsh, vous pouvez également le faire : alias duc='du -sh *(/)'

0 votes

@cbliard C'est toujours bogué. Essayez de l'exécuter avec un nom de fichier contenant, par exemple, une séquence de tabulation ou plusieurs espaces ; vous remarquerez qu'il les transforme tous en un seul espace, car vous ne citez pas dans votre écho. Et puis il y a le cas des noms de fichiers contenant des nouvelles lignes...

7voto

Jonathan Leffler Points 299946

Cette opération est extrêmement délicate sous Unix standard, et la plupart des solutions se heurtent aux nouvelles lignes ou à un autre caractère. Cependant, si vous utilisez l'ensemble des outils GNU, vous pouvez exploiter la fonction find option -print0 et utiliser xargs avec l'option correspondante -0 (moins-zéro). Il y a deux caractères qui ne peuvent pas apparaître dans un nom de fichier simple ; ce sont les barres obliques et NUL ' \0 '. Évidemment, le slash apparaît dans les noms de chemin, donc la solution GNU d'utiliser un NUL ' \0 pour marquer la fin du nom est ingénieux et infaillible.

4voto

Gordon Davisson Points 22534

Ne stockez pas les listes sous forme de chaînes de caractères, mais sous forme de tableaux, afin d'éviter toute confusion au niveau des délimiteurs. Voici un exemple de script qui opérera soit sur tous les sous-répertoires de test, soit sur la liste fournie sur sa ligne de commande :

#!/bin/bash
if [ $# -eq 0 ]; then
        # if no args supplies, build a list of subdirs of test/
        dirlist=() # start with empty list
        for f in test/*; do # for each item in test/ ...
                if [ -d "$f" ]; then # if it's a subdir...
                        dirlist=("${dirlist[@]}" "$f") # add it to the list
                fi
        done
else
        # if args were supplied, copy the list of args into dirlist
        dirlist=("$@")
fi
# now loop through dirlist, operating on each one
for dir in "${dirlist[@]}"; do
        printf "Directory: %s\n" "$dir"
done

Essayons maintenant sur un répertoire de test avec une ou deux courbes :

$ ls -F test
Baltimore/
Cherry Hill/
Edison/
New York City/
Philadelphia/
this is a dirname with quotes, lfs, escapes: "\''?'?\e\n\d/
this is a file, not a directory
$ ./test.sh 
Directory: test/Baltimore
Directory: test/Cherry Hill
Directory: test/Edison
Directory: test/New York City
Directory: test/Philadelphia
Directory: test/this is a dirname with quotes, lfs, escapes: "\''
'
\e\n\d
$ ./test.sh "Cherry Hill" "New York City"
Directory: Cherry Hill
Directory: New York City

1 votes

En y repensant, il y a en fait était une solution avec POSIX sh : Vous pourriez réutiliser le "$@" en l'ajoutant avec set -- "$@" "$f" .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X