boucle dans les résultats `ls` dans le script shell bash

94

Quelqu'un a-t-il un modèle de script shell permettant de créer lsune liste de noms de répertoires et de les parcourir en boucle?

Je prévois de faire ls -1d */pour obtenir la liste des noms de répertoires.

Daniel A. White
la source

Réponses:

111

Ne pas utiliser ls à la place d'un glob, comme @ shawn-j-goff et d'autres l'ont suggéré.

Il suffit d'utiliser une for..do..doneboucle:

for f in *; do
  echo "File -> $f"
done

Vous pouvez remplacer le *with *.txtou tout autre glob qui retourne une liste (de fichiers, répertoires ou quoi que ce soit d'autre), une commande qui génère une liste, par exemple $(cat filelist.txt), ou bien la remplacer par une liste.

Dans la doboucle, vous faites simplement référence à la variable de boucle avec le préfixe du signe dollar (comme $fdans l'exemple ci-dessus). Vous pouvez echole faire ou faire tout ce que vous voulez.

Par exemple, pour renommer tous les .xmlfichiers du répertoire en cours en .txt:

for x in *.xml; do 
  t=$(echo $x | sed 's/\.xml$/.txt/'); 
  mv $x $t && echo "moved $x -> $t"
done

Ou mieux encore, si vous utilisez Bash, vous pouvez utiliser les extensions de paramètres Bash plutôt que de générer un sous-shell:

for x in *.xml; do 
  t=${x%.xml}.txt
  mv $x $t && echo "moved $x -> $t"
done
CoverosGene
la source
22
Que se passe-t-il si le nom du fichier contient un espace?
Daniel A. White
4
Malheureusement, comme l'a dit Daniel, le code dans cette réponse sera rompu si l'un des fichiers ou des dossiers contient un espace ou une nouvelle ligne dans leur nom. Cela montre un très mauvais usage des forboucles et le piège typique d’essayer d’analyser la sortie de ls. @ DanielA.White, vous pouvez envisager d’accepter cette réponse si elle n’est pas utile (ou si elle est potentiellement trompeuse), car, comme vous l’avez dit, vous agissez sur des annuaires. La réponse de Shawn J. Goff devrait fournir une solution plus robuste et plus efficace à votre problème.
Slhck
4
-∞ Vous ne devriez pas analyser la lssortie , vous ne devriez pas lire la sortie dans une forboucle , vous devriez utiliser à la $()place de `` et Use More Quotes ™ . Je suis sûr que @CoverosGene était bien intentionné, mais c’est terrible.
l0b0
1
Une meilleure alternative si vous voulez vraiment utiliser lsest ls -1 | while read line; do stuff; done. Au moins celui-ci ne cassera pas pour les espaces blancs.
ejoubaud
1
avec mv -vvous n'avez pas besoinecho "moved $x -> $t"
DmitrySandalov
69

Utiliser la sortie de lspour obtenir les noms de fichiers est une mauvaise idée . Cela peut conduire à des scripts défectueux et même dangereux. En effet , un nom de fichier peut contenir des caractères à l' exception /et le nullcaractère, et lsne pas utiliser l' un de ces personnages comme délimiteurs, donc si un nom de fichier a un espace ou un saut de ligne, vous sera obtenir des résultats inattendus.

Il existe deux très bons moyens de parcourir les fichiers. Ici, j’ai simplement utilisé echopour montrer comment faire quelque chose avec le nom de fichier; vous pouvez utiliser n'importe quoi, cependant.

La première consiste à utiliser les fonctions de globbing natives du shell.

for dir in */; do
  echo "$dir"
done

Le shell se développe */en arguments séparés lus par la forboucle; même s'il y a un espace, une nouvelle ligne ou tout autre caractère étrange dans le nom du fichier, forchaque nom complet sera vu comme une unité atomique; ce n'est pas analyser la liste en aucune façon.

Si vous voulez aller récursivement dans les sous-répertoires, cela ne fonctionnera que si votre shell dispose de fonctionnalités étendues (telles que celles bashde globstar.). Si votre shell n'a pas ces fonctionnalités, ou si vous voulez vous assurer que votre script fonctionnera Sur une variété de systèmes, l'option suivante consiste à utiliser find.

find . -type d -exec echo '{}' \;

Ici, la findcommande appellera echoet lui passera un argument du nom de fichier. Il le fait une fois pour chaque fichier trouvé. Comme dans l'exemple précédent, il n'y a pas d'analyse d'une liste de noms de fichiers; au lieu de cela, un nom de fichier est envoyé complètement comme argument.

La syntaxe de l' -execargument semble un peu drôle. findprend le premier argument après -execet traite cela comme le programme à exécuter, et chaque argument suivant, il le prend comme argument à transmettre à ce programme. Il y a deux arguments spéciaux -execà voir. Le premier est {}; cet argument est remplacé par un nom de fichier findgénéré par les parties précédentes . La seconde est ;, ce qui permet de findsavoir que ceci est la fin de la liste des arguments à transmettre au programme; finda besoin de cela car vous pouvez continuer avec plus d’arguments finddestinés au programme exec'd et non au programme exec'd. La raison en \est que la coquille traite également;spécialement - il représente la fin d'une commande, nous avons donc besoin de l'échapper pour que le shell l'envoie comme argument findplutôt que de le consommer pour lui-même; Une autre façon de faire en sorte que la coquille ne la traite pas spécialement est de la mettre entre guillemets: cela ';'fonctionne aussi bien que \;pour le faire.

Shawn J. Goff
la source
2
+1 c'est vraiment la voie à suivre lorsque vous devez générer une liste de fichiers et l'utiliser dans une commande. find -exec est limité au seul fait de pouvoir exécuter une seule commande. Avec la boucle, vous pouvez accéder au contenu de votre coeur.
MaQleod
+1 Je souhaite que plus de gens utilisent find. Il est possible de faire de la magie et même de -execcontourner les limites perçues . Cette -print0option est également utile à utiliser avec xargs.
Ghoti
L'option de boucle ne montrera pas les répertoires cachés. L'option de recherche ne montre pas les liens symboliques.
Jinawee
16

Pour les fichiers contenant des espaces, vous devrez vous assurer de citer la variable comme suit:

 for i in $(ls); do echo "$i"; done; 

ou, vous pouvez modifier la variable d'environnement IFS (Input Field Separator):

 IFS=$'\n';for file in $(ls); do echo $i; done

Enfin, selon ce que vous faites, vous n’avez peut-être même pas besoin du ls:

 for i in *; do echo "$i"; done;
John
la source
belle utilisation d'un sous-shell dans le premier exemple
Jeremy L
Pourquoi IFS a-t-il besoin d'un $ après l'affectation et avant le nouveau personnage?
Andy Ibanez
3
Les noms de fichiers peuvent également contenir des nouvelles lignes. La rupture \nest insuffisante. Ce n'est jamais une bonne idée de recommander l'analyse de la sortie de ls.
Ghoti
4

Pour ajouter à la réponse de CoverosGene, voici un moyen de ne lister que les noms de répertoires:

for f in */; do
  echo "Directory -> $f"
done
n3o
la source
1

Pourquoi ne pas définir IFS sur une nouvelle ligne, puis capturer la sortie de lsdans un tableau? Définir IFS sur newline devrait résoudre les problèmes de caractères amusants dans les noms de fichiers; utiliser lspeut être agréable car il dispose d’une fonction de tri intégrée.

(Lors des tests, j'avais du mal à définir IFS sur, \nmais le réglage sur newline backspace fonctionne, comme suggéré ailleurs ici):

Par exemple (en supposant que le lsmodèle de recherche souhaité a été transmis $1):

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")

FILES=($(/bin/ls "$1"))

for AFILE in ${FILES[@]}
do
    ... do something with a file ...
done

IFS=$SAVEIFS

Ceci est particulièrement pratique sous OS X, par exemple, pour capturer une liste de fichiers triés par date de création (du plus ancien au plus récent), la lscommande est ls -t -U -r.

jetset
la source
2
Les noms de fichiers peuvent également contenir des sauts de lignes, ce qui est souvent le cas lorsque les utilisateurs sont autorisés à nommer leurs propres fichiers. La rupture \nest insuffisante. Les seules solutions valides utilisent une boucle for avec extension de nom de chemin, ou find.
Ghoti
Le seul moyen fiable de transférer une liste de noms de fichiers est de les séparer avec un caractère NUL, car c'est le seul qui ne figure pas dans un chemin de fichier.
Glglgl
-3

Voici comment je le fais, mais il existe probablement des moyens plus efficaces.

ls > filelist.txt

while read filename; do echo filename: "$filename"; done < filelist.txt
ARBRE
la source
6
Tenez-vous en aux tuyaux à la place du fichier:>ls | while read i; do echo filename: $i; done
Jeremy L
Cool. Je devrais dire que vous pouvez également utiliser $ EDITOR filelist.txt entre les deux commandes. Beaucoup de choses que vous pouvez faire dans un éditeur plus facile que sur la ligne de commande. Pas pertinent pour cette question, cependant.
ARBRE
Votre solution ne résout pas du tout le problème des noms de fichiers contenant des sauts de lignes et autres éléments de fantaisie.
Glglgl