web-dev-qa-db-fra.com

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

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

 test/Baltimore 
 test/Cherry 
 Hill 
 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 faire cela.


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 veux plutôt passer les noms de répertoires en tant que paramètres de ligne de commande.

J'ai pris la suggestion de Charles de régler l'IFS et 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 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, appelez le script comme suit: test.sh "Cherry Hill" "New York City" produit la sortie suivante:

 Cherry 
 Hill 
 Nouveau 
 York 
 Ville 
117
MCS

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

# 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 trouve -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 le transmettre ultérieurement:

# 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 trouvaille ne supporte pas -print0, votre résultat est alors dangereux - les éléments ci-dessous ne se comporteront pas comme souhaité si 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 vous n'utilisez pas l’un des éléments ci-dessus, une troisième approche (moins efficace en termes de temps et d’utilisation de la mémoire, car elle lit la sortie entière du sous-processus avant de procéder au fractionnement de Word) consiste à utiliser un IFS variable qui ne contient pas le caractère d'espacement. Désactiver le globbing (set -f) pour empêcher les chaînes contenant des caractères globaux tels que [], * ou ? d'être étendu:

# 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 du paramètre 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 important. Les tableaux peuvent également être peuplés d'autres manières, telles que les expressions globales:

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

Cependant, cela 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 souhaitez réellement avoir le nom du 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: '
25

Voici une solution simple qui gère les tabulations et/ou les espaces dans le nom du fichier. Si vous devez traiter d'autres caractères étranges dans le nom de fichier, tels que des nouvelles lignes, 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 du fichier doit être cité ("$f") si utilisé comme argument. Sans guillemets, les espaces servent de séparateur d'arguments et plusieurs arguments sont attribués à la commande invoquée.

Et la sortie:

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

Ceci est extrêmement délicat sous Unix standard, et la plupart des solutions se basent sur les nouvelles lignes ou sur un autre personnage. Toutefois, si vous utilisez l'ensemble d'outils GNU, vous pouvez 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 slash et NUL '\ 0'. De toute évidence, les barres obliques apparaissent dans les noms de chemins, de sorte que la solution GNU consistant à utiliser un NUL '\ 0' pour marquer la fin du nom est ingénieuse et infaillible.

7
Jonathan Leffler

Vous pouvez utiliser temporairement IFS (séparateur de champs interne) 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
4
amazingthere

Pourquoi ne pas simplement mettre

IFS='\n'

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

4
oshunluvr

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

4
murpel

Ne stockez pas les listes sous forme de chaînes; stockez-les 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 ou deux courbes inséré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
4
Gordon Davisson
find . -print0|while read -d $'\0' file; do echo "$file"; done
4
Freakus

ps si ce n’est que de l’espace dans l’entrée, alors quelques guillemets doubles se sont bien déroulés pour moi ...

read artist;

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

Pour ajouter à quoi Jonathan dit: utilisez le -print0 option pour find en conjonction avec xargs comme suit:

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

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

2
Adam Rosenfield

Il fallait aussi traiter les espaces dans les noms de chemin. 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
}
1
Florian Bender
#!/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 sont dans des dossiers différents et les noms des dossiers ont espaces blancs aussi. Mon script ci-dessus convertira les fichiers .mov en fichiers .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é!

1
Sony George

Convertissez la liste de fichiers en 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 de convertir n'importe quelle entrée multiligne en un tableau de 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 ligne de commande de Bash ou utiliser de grandes quantités de mémoire.

Les approches dans lesquelles la boucle qui fonctionne finalement sur la liste ont également été acheminées ont l'inconvénient que la lecture de stdin n'est pas facile (par exemple, demander à l'utilisateur d'entrer) et que la boucle est un nouveau processus. Vous vous demandez peut-être pourquoi les variables vous définissez à l'intérieur de la boucle ne sont pas disponibles après la fin de la boucle.

Je n'aime pas non plus définir IFS, cela peut gâcher un autre code.

1
Steve Zobell

je viens de découvrir qu'il existe certaines similitudes entre mon question et le vôtre. Apparemment si vous voulez passer des arguments dans des commandes

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

les imprimer dans l'ordre

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

remarquez que le $ @ est entouré de guillemets, de notes ici

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

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 dans le dossier actuel et que certains d'entre eux contenaient des espaces.

Je l'ai abordé ainsi:

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

Semble beaucoup simple pour moi

0
Tebe

J'avais besoin du même concept pour compresser séquentiellement plusieurs répertoires ou fichiers d'un dossier donné. J'ai résolu l'utilisation d'awk pour sélectionner la liste de ls et éviter le problème de l'espace 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?

0
Hìr0