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

J’ai un script shell bash qui parcourt tous les répertoires enfants (mais pas les fichiers) d’un répertoire donné. Le problème est que certains noms de répertoires 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 parcourt les répertoires:

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

Voici le résultat:

 test / Baltimore
 test / cerise
 Colline
 test / Edison 
 test / Nouveau
 York
 Ville
 test / Philadelphie

Cherry Hill et New York City sont traitées 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 le faire.


Les réponses ci-dessous sont excellentes. Mais pour compliquer les choses, je ne veux pas toujours utiliser les répertoires listés dans mon répertoire de test. Parfois, je souhaite transférer les noms de répertoires en tant que parameters de ligne de commande à la place.

J’ai pris la suggestion de Charles de mettre en place l’IFS et j’ai proposé ce qui suit:

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

et cela fonctionne très bien 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, l’appel du script comme ceci: test.sh "Cherry Hill" "New York City" produit la sortie suivante:

 Cerise
 Colline
 Nouveau
 York
 Ville

Tout d’abord, ne le faites pas de cette façon. La meilleure approche consiste à utiliser correctement find -exec :

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

L’autre approche sûre consiste à utiliser la liste terminée par NUL, bien que cela nécessite que votre support de recherche -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 transmettre 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 recherche ne prend pas en charge -print0 , votre résultat est alors dangereux: le -print0 ci-dessous ne se comportera pas comme souhaité si des fichiers contiennent des nouvelles lignes dans leurs noms (ce qui, oui, est légal):

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

Si l’on n’utilise pas l’une des options ci-dessus, une troisième approche (moins efficace en termes de temps et d’utilisation de la mémoire, car elle lit la totalité de la sortie du sous-processus avant de séparer les mots) consiste à utiliser une variable IFS ne contient pas le caractère d'espace. Désactivez le globbing ( set -f ) pour empêcher les chaînes contenant des caractères globaux tels que [] , * ou ? d'être élargi:

 # 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 le paramètre paramètre de ligne de commande, vous devriez utiliser des tableaux si votre shell les prend en charge (c'est-à-dire ksh, bash ou zsh):

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

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

 # this is safe ensortinges=( test/* ) for d in "${ensortinges[@]}"; do printf '%s\n' "$d" done 
 find . -type d | while read file; do echo $file; done 

Cependant, ne fonctionne pas si le nom du fichier contient des nouvelles lignes. Ce qui précède est la seule solution que je connaisse lorsque vous voulez réellement avoir le nom de répertoire dans une variable. Si vous voulez juste exécuter une commande, utilisez xargs.

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

Voici une solution simple qui gère les tabs et / ou les espaces blancs du nom de fichier. Si vous devez traiter d’autres caractères étranges du nom de fichier, comme newlines, choisissez une autre réponse.

Le répertoire de test

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

Le code pour aller dans les répertoires

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

Le nom de fichier doit être cité ( "$f" ) s’il est utilisé comme argument. Sans les guillemets, les espaces agissent comme séparateur d’arguments et plusieurs arguments sont donnés à la commande invoquée.

Et la sortie:

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

Ceci est extrêmement compliqué avec Unix standard, et la plupart des solutions ne respectent pas les nouvelles lignes ou un autre caractère. Cependant, si vous utilisez le jeu d’outils GNU, vous pouvez alors exploiter l’option find -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 des barres obliques et NUL ‘\ 0’. De toute évidence, la barre oblique apparaît dans les noms de chemin, donc la solution GNU consistant à utiliser un NUL ‘\ 0’ pour marquer la fin du nom est ingénieuse et infaillible.

Pourquoi ne pas simplement mettre

 IFS='\n' 

devant la commande? Cela change le séparateur de champs de à

j’utilise

 SAVEIFS=$IFS IFS=$(echo -en "\n\b") for f in $( find "$1" -type d ! -path "$1" ) do echo $f done IFS=$SAVEIFS 

Ne serait-ce pas suffisant?
Idée tirée de http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html

Ne stockez pas les listes en tant que chaînes; stockez-les en tant que tableaux pour éviter toute confusion de délimiteur. Voici un exemple de script qui opère 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 

Maintenant, essayons ceci sur un répertoire de test avec une courbe ou deux dans:

 $ 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 
 find . -print0|while read -d $'\0' file; do echo "$file"; done 

ps si ce n’est que de l’espace dans l’entrée, alors quelques guillemets doubles ont fonctionné sans problème pour moi …

 read artist; find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \; 

Vous pouvez utiliser IFS (séparateur de champ interne) en utilisant temporairement:

 OLD_IFS=$IFS # Stores Default IFS IFS=$'\n' # Set it to line break for f in `find test/* -type d`; do echo $f done $IFS=$OLD_IFS 

Pour append à ce que Jonathan a dit: utilisez l’option -print0 pour find en conjonction avec xargs comme suit:

 find test/* -type d -print0 | xargs -0 command 

Cela exécutera la commande de command avec les arguments appropriés; les répertoires contenant des espaces seront correctement cités (c’est-à-dire qu’ils seront transmis en un seul argument).

 #!/bin/bash dirtys=() for folder in * do if [ -d "$folder" ]; then dirtys=("${dirtys[@]}" "$folder") fi done for dir in "${dirtys[@]}" do for file in "$dir"/\*.mov # <== *.mov do #dir_e=`echo "$dir" | sed 's/[[:space:]]/\\\ /g'` -- This line will replace each space into '\ ' out=`echo "$file" | sed 's/\(.*\)\/\(.*\)/\2/'` # These two line code can be written in one line using multiple sed commands. out=`echo "$out" | sed 's/[[:space:]]/_/g'` #echo "ffmpeg -i $out_e -sameq -vcodec msmpeg4v2 -acodec pcm_u8 $dir_e/${out/%mov/avi}" `ffmpeg -i "$file" -sameq -vcodec msmpeg4v2 -acodec pcm_u8 "$dir"/${out/%mov/avi}` done done 

Le code ci-dessus convertira les fichiers .mov en .avi. Les fichiers .mov se trouvent dans des dossiers différents et les noms de dossiers comportent également des espaces blancs . Mon script ci-dessus convertira les fichiers .mov en fichier .avi dans le même dossier. Je ne sais pas si cela vous aide les peuples.

Cas:

 [sony@localhost shell_tutorial]$ ls Chapter 01 - Introduction Chapter 02 - Your First Shell Script [sony@localhost shell_tutorial]$ cd Chapter\ 01\ -\ Introduction/ [sony@localhost Chapter 01 - Introduction]$ ls 0101 - About this Course.mov 0102 - Course Structure.mov [sony@localhost Chapter 01 - Introduction]$ ./above_script ... successfully executed. [sony@localhost Chapter 01 - Introduction]$ ls 0101_-_About_this_Course.avi 0102_-_Course_Structure.avi 0101 - About this Course.mov 0102 - Course Structure.mov [sony@localhost Chapter 01 - Introduction]$ CHEERS! 

À votre santé!

Il fallait aussi avoir affaire à des espaces blancs dans les noms de chemins. J’ai finalement utilisé une récursivité et for item in /path/* :

 function recursedir { local item for item in "${1%/}"/* do if [ -d "$item" ] then recursedir "$item" else command fi done } 

Convertissez la liste de fichiers en un tableau Bash. Ceci utilise l’approche de Matt McClure pour retourner un tableau à partir d’une fonction Bash: http://notes-matthewlmcclure.blogspot.com/2009/12/return-array-from-bash-function-v-2.html Le résultat est un moyen convertir toute entrée multiligne en un tableau Bash.

 #!/bin/bash # This is the command where we want to convert the output to an array. # Output is: fileSize fileNameIncludingPath multiLineCommand="find . -mindepth 1 -printf '%s %p\\n'" # This eval converts the multi-line output of multiLineCommand to a # Bash array. To convert stdin, remove: < <(eval "$multiLineCommand" ) eval "declare -a myArray=`( arr=(); while read -r line; do arr[${#arr[@]}]="$line"; done; declare -p arr | sed -e 's/^declare -a arr=//' ) < <(eval "$multiLineCommand" )`" for f in "${myArray[@]}" do echo "Element: $f" done 

Cette approche semble fonctionner même lorsque des caractères incorrects sont présents et constitue un moyen général de convertir toute entrée en tableau Bash. L'inconvénient est que si l'entrée est longue, vous pouvez dépasser les limites de taille de la ligne de commande de Bash ou utiliser de grandes quantités de mémoire.

Les approches où la boucle qui travaille éventuellement sur la liste est également désavantagée par le fait que la lecture de stdin n'est pas facile (par exemple, demander des entrées à l'utilisateur) et la boucle est un nouveau processus. vous définissez à l'intérieur de la boucle ne sont pas disponibles une fois la boucle terminée.

Je n'aime pas non plus la configuration de l'IFS, cela peut perturber les autres codes.

Je viens de découvrir qu’il existe des similitudes entre ma question et la vôtre. Aparrently si vous voulez passer des arguments dans les commandes

 test.sh "Cherry Hill" "New York City" 

les imprimer dans l’ordre

 for SOME_ARG in "$@" do echo "$SOME_ARG"; done; 

Remarquez que $ @ est entouré de guillemets, quelques notes ici

J’avais besoin du même concept pour compresser séquentiellement plusieurs répertoires ou fichiers à partir d’un certain dossier. J’ai résolu l’utilisation de awk pour parsemer la liste de ls et éviter le problème d’espace vide dans le nom.

 source="/xxx/xxx" dest="/yyy/yyy" n_max=`ls . | wc -l` echo "Loop over items..." i=1 while [ $i -le $n_max ];do item=`ls . | awk 'NR=='$i'' ` echo "File selected for compression: $item" tar -cvzf $dest/"$item".tar.gz "$item" i=$(( i + 1 )) done echo "Done!!!" 

Qu’est-ce que tu penses?

 find Downloads -type f | while read file; do printf "%q\n" "$file"; done 

Eh bien, je vois trop de réponses compliquées. Je ne veux pas passer la sortie de find utility ou écrire une boucle, car find a l’option “exec” pour cela.

Mon problème était que je voulais déplacer tous les fichiers avec l’extension dbf dans le dossier actuel et que certains d’entre eux contenaient des espaces blancs.

Je l’ai abordé pour:

  find . -name \*.dbf -print0 -exec mv '{}' . ';' 

Ça me semble très simple

Pour moi, cela fonctionne, et c’est plutôt “propre”:

 for f in "$(find ./test -type d)" ; do echo "$f" done 

Juste un problème de variante simple … Convertir des fichiers de .flv typé en .mp3 (bâillement).

 for file in read `find . *.flv`; do ffmpeg -i ${file} -acodec copy ${file}.mp3;done 

trouver de manière récursive tous les fichiers flash des utilisateurs Macintosh et les transformer en audio (copier, pas de transcodage) … c’est comme au-dessus, en notant que la lecture au lieu de simplement “pour le fichier dans” ‘va échapper.