Comment puis-je échapper à un espace blanc dans une liste de boucles bash?

121

J'ai un script shell bash qui parcourt 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 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 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 rendre cela plus compliqué, je ne veux pas toujours utiliser les répertoires répertoriés dans mon répertoire de test. Parfois, je veux passer les noms de répertoire en tant que paramètres de ligne de commande à la place.

J'ai pris la suggestion de Charles de configurer l'IFS et j'ai trouvé 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 entre guillemets). Par exemple, appeler le script comme ceci: test.sh "Cherry Hill" "New York City"produit la sortie suivante:

Cerise
Colline
Nouveau
York
Ville
MCS
la source
re: edit, list="$@"rejette complètement la liste de la valeur d'origine, la réduisant en une chaîne. Veuillez suivre les pratiques de ma réponse exactement telles qu'elles ont été données - une telle affectation n'est encouragée nulle part; si vous souhaitez transmettre une liste d'arguments de ligne de commande à un programme, vous devez les rassembler dans un tableau et développer ce tableau directement.
Charles Duffy

Réponses:

105

Premièrement, ne le faites pas de cette façon. La meilleure approche consiste à utiliser find -execcorrectement:

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

L'autre approche sûre consiste à utiliser une liste terminée par NUL, bien que cela nécessite que votre recherche prenne en charge -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 - ce qui suit ne se comportera pas comme vous le souhaitez si des fichiers contiennent des retours à la ligne 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 d'utilisation du temps et de la mémoire, car elle lit toute la sortie du sous-processus avant de procéder au fractionnement de mots) consiste à utiliser une IFSvariable qui ne ne contient pas le caractère espace. Désactivez le globbing ( set -f) pour empêcher les chaînes contenant des caractères glob tels que [], *ou ?d'être développées:

# 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 cas des paramètres de ligne de commande, vous devriez utiliser des tableaux si votre shell les prend en charge (c'est-à-dire que c'est 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 $@plutôt que $*) est importante. Les tableaux peuvent également être remplis d'autres manières, telles que les expressions glob:

# this is safe
entries=( test/* )
for d in "${entries[@]}"; do
  printf '%s\n' "$d"
done
Charles Duffy
la source
1
ne connaissait pas cette saveur «+» pour -exec. sweet
Johannes Schaub - litb
1
tho on dirait qu'il peut aussi, comme xargs, ne mettre les arguments qu'à la fin de la commande donnée: / ça m'a parfois
dérangé
Je pense que -exec [nom] {} + est une extension GNU et 4.4-BSD. (Au moins, il n'apparaît pas sur Solaris 8, et je ne pense pas que ce soit dans AIX 4.3 non plus.) Je suppose que le reste d'entre nous est peut-être coincé avec une tuyauterie vers xargs ...
Michael Ratanapintha
2
Je n'ai jamais vu la syntaxe $ '\ n' auparavant. Comment ça marche? (J'aurais pensé que IFS = '\ n' ou IFS = "\ n" fonctionnerait, mais ni l'un ni l'autre.)
MCS
1
@crosstalk c'est définitivement dans Solaris 10, je viens de l'utiliser.
Nick
26
find . -type d | while read file; do echo $file; done

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

find . -type d -print0 | xargs -0 echo 'The directory is: '
Johannes Schaub - litb
la source
Pas besoin de xargs, voir find -exec ... {} +
Charles Duffy
4
@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, qui peut être un ordre de grandeur plus lent.
Adam Rosenfield
1
J'aime plus les xargs. Ces deux semblent essentiellement faire la même chose, tandis que xargs a plus d'options, comme courir en parallèle
Johannes Schaub - litb
2
Adam, non que «+» on agrégera autant de noms de fichiers que possible puis s'exécutera. mais il n'aura pas des fonctions aussi
intéressantes
2
Notez que si vous voulez faire quelque chose avec les noms de fichiers, vous allez devoir les citer. Ex:find . -type d | while read file; do ls "$file"; done
David Moles
23

Voici une solution simple qui gère les tabulations et / ou les espaces dans le nom de fichier. Si vous devez gérer d'autres caractères étranges dans le nom de fichier, comme des sauts de ligne, 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 entre guillemets ( "$f") s'il est utilisé comme argument. Sans guillemets, les espaces servent de séparateur d'arguments et plusieurs arguments sont donnés à la commande appelée.

Et la sortie:

test/Baltimore
test/Cherry Hill
test/Edison
test/New York City
test/Philadelphia
cbliard
la source
merci, cela a fonctionné pour l'alias que je créais pour lister l'espace utilisé par chaque répertoire dans le dossier actuel, il étouffait certains répertoires avec des espaces dans l'incarnation précédente. Cela fonctionne dans zsh, mais certaines des autres réponses ne l'ont pas fait:alias duc='ls -d * | while read D; do du -sh "$D"; done;'
Ted Naleid
2
Si vous utilisez zsh, vous pouvez également faire ceci:alias duc='du -sh *(/)'
cbliard
@cbliard C'est toujours bogué. Essayez de l'exécuter avec un nom de fichier avec, par exemple, une séquence de tabulations ou plusieurs espaces; vous noterez que cela change n'importe lequel d'entre eux 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 ...
Charles Duffy
@CharlesDuffy J'ai essayé avec des séquences d'onglets et plusieurs espaces. Cela fonctionne avec des citations. J'ai aussi essayé avec les nouvelles lignes et cela ne fonctionne pas du tout. J'ai mis à jour la réponse en conséquence. Merci de l'avoir signalé.
cbliard
1
@cbliard Droite - ajouter des guillemets à votre commande echo était ce à quoi je voulais en venir. En ce qui concerne les nouvelles lignes, vous pouvez faire fonctionner cela en utilisant find -print0et IFS='' read -r -d '' f.
Charles Duffy
7

C'est extrêmement délicat dans Unix standard, et la plupart des solutions se heurtent à des sauts de ligne ou à un autre caractère. Cependant, si vous utilisez l'ensemble d'outils GNU, vous pouvez exploiter l' findoption -print0et l'utiliser xargsavec 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, une barre oblique apparaît dans les noms de chemin, donc la solution GNU d'utiliser un NUL '\ 0' pour marquer la fin du nom est ingénieuse et infaillible.

Jonathan Leffler
la source
4

Pourquoi ne pas simplement mettre

IFS='\n'

devant la commande for? Cela change le séparateur de champ de <Space> <Tab> <Newline> à juste <Newline>

oshunluvr
la source
4
find . -print0|while read -d $'\0' file; do echo "$file"; done
Freakus
la source
1
-d $'\0'est exactement le même que -d ''- parce que bash utilise des chaînes terminées par NUL, le premier caractère d'une chaîne vide est un NUL, et pour la même raison, les NUL ne peuvent pas du tout être représentés à l'intérieur des chaînes C.
Charles Duffy
4

j'utilise

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

Cela ne suffirait-il pas?
Idée tirée de http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html

murpel
la source
bon conseil: c'est très utile pour les options d'un osascript en ligne de commande (OS X AppleScript), où les espaces divisent un argument en plusieurs paramètres où un seul est prévu
tim
Non, ça ne suffit pas. Il est inefficace (en raison de l'utilisation inutile de $(echo ...)), ne gère pas correctement les noms de fichiers avec des expressions glob, ne gère pas correctement les noms de fichiers qui contiennent $'\b'ou $ '\ n' caractères, et de plus convertit plusieurs exécutions d'espaces en caractères d'espaces uniques sur le côté sortie en raison d'un devis incorrect.
Charles Duffy
4

Ne stockez pas les listes sous forme de chaînes; les stocker sous forme de tableaux pour éviter toute cette confusion de délimiteur. Voici un exemple de script qui fonctionnera 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 ceci sur un répertoire de test avec une courbe ou deux lancées:

$ 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
Gordon Davisson
la source
1
Rétrospectivement, il y avait en fait une solution avec POSIX sh: vous pouviez réutiliser le "$@"tableau, en y ajoutant set -- "$@" "$f".
Charles Duffy
4

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

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

étonnant ici
la source
Veuillez fournir une explication.
Steve K
IFS a spécifié quel est le symbole de séparateur, puis le nom de fichier avec un espace blanc ne serait pas tronqué.
amazingthere
$ IFS = $ OLD_IFS à la fin devrait être: IFS = $ OLD_IFS
Michel Donais le
3

ps s'il ne s'agit que d'espace dans l'entrée, alors des guillemets doubles ont bien fonctionné pour moi ...

read artist;

find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \;
dur mais pas
la source
2

Pour ajouter à ce que Jonathan a dit: utilisez l' -print0option pour finden conjonction avec ce xargsqui suit:

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

Cela exécutera la commande commandavec les arguments appropriés; les répertoires contenant des espaces seront correctement cités (c'est-à-dire qu'ils seront passés comme un argument).

Adam Rosenfield
la source
1
#!/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 différents dossiers et les noms de dossier ont des espaces blancs . Mon script ci-dessus convertira les fichiers .mov en fichier .avi dans le même dossier lui-même. 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é!

Sony George
la source
echo "$name" | ...ne fonctionne pas si namec'est le cas -n, et comment il se comporte avec des noms avec des séquences d'échappement anti-slash dépend de votre implémentation - POSIX rend le comportement de echodans ce cas explicitement indéfini (alors que POSIX étendu par XSI rend l'expansion des séquences d'échappement anti-slash standard , et les systèmes GNU - y compris bash - sans POSIXLY_CORRECT=1rompre le standard POSIX en implémentant -e(alors que la spécification nécessite echo -ed'imprimer -een sortie). printf '%s\n' "$name" | ...est plus sûr.
Charles Duffy
1

Il fallait aussi traiter des espaces dans les chemins. Ce que j'ai finalement fait, c'est d'utiliser 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
}
Gilles 'SO- arrête d'être méchant'
la source
1
N'utilisez pas le functionmot-clé - cela rend votre code incompatible avec POSIX sh, mais n'a aucun autre but utile. Vous pouvez simplement définir une fonction avec recursedir() {, en ajoutant les deux parenthèses et en supprimant le mot-clé de fonction, et cela sera compatible avec tous les shells compatibles POSIX.
Charles Duffy
1

Convertissez la liste de fichiers en un tableau Bash. Cela utilise l'approche de Matt McClure pour renvoyer 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 pour convertir n'importe quelle 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 de mauvais caractères sont présents et constitue un moyen général de convertir toute entrée en un tableau Bash. L'inconvénient est que si l'entrée est longue, vous pourriez dépasser les limites de taille de ligne de commande de Bash ou utiliser de grandes quantités de mémoire.

Les approches où la boucle qui travaille finalement sur la liste a également la liste insérée ont l'inconvénient que la lecture de stdin n'est pas facile (comme demander à l'utilisateur une entrée), et la boucle est un nouveau processus, vous vous demandez peut-être pourquoi les variables que 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 configurer IFS, cela peut gâcher d'autres codes.

Steve Zobell
la source
Si vous utilisez IFS='' read, sur la même ligne, le paramètre IFS est présent uniquement pour la commande de lecture et ne lui échappe pas. Il n'y a aucune raison de ne pas aimer configurer IFS de cette manière.
Charles Duffy
1

Eh bien, je vois trop de réponses compliquées. Je ne veux pas passer la sortie de l'utilitaire find 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 vers le dossier actuel et certains d'entre eux contenaient un espace blanc.

Je l'ai abordé ainsi:

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

Cela me semble bien simple

Tebe
la source
0

je viens de découvrir qu'il existe des similitudes entre ma question et la vôtre. Aparentement si vous voulez passer des arguments dans des commandes

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

pour les imprimer dans l'ordre

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

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

Jeffrey04
la source
0

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 en utilisant awk pour ramasser 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?

Hìr0
la source
Je pense que cela ne fonctionnera pas correctement si les noms de fichiers ont des nouvelles lignes. Vous devriez peut-être l'essayer.
user000001
0
find Downloads -type f | while read file; do printf "%q\n" "$file"; done
Johan Kasselman
la source
-3

Pour moi, cela fonctionne, et c'est à peu près "propre":

for f in "$(find ./test -type d)" ; do
  echo "$f"
done
AndrzejP
la source
4
Mais c'est pire. Les guillemets doubles autour de la recherche provoquent la concaténation de tous les noms de chemin en une seule chaîne. Changez l' écho en ls pour voir le problème.
NVRAM
-4

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

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

trouver récursivement tous les fichiers flash de l'utilisateur Macintosh et les transformer en audio (copie, pas de transcodage) ... c'est comme ci-dessus, notant que lire au lieu de juste «pour fichier dans » s'échappera.

Mark Washeim
la source
2
L' readaprès inest un mot de plus dans la liste sur laquelle vous répétez. Ce que vous avez publié est une version légèrement cassée de ce que le demandeur avait, qui ne fonctionne pas. Vous avez peut-être eu l'intention de publier quelque chose de différent, mais cela est probablement couvert par d'autres réponses ici de toute façon.
Gilles 'SO- arrête d'être diabolique'