Comment appliquer une commande shell à de nombreux fichiers dans les sous-répertoires imbriqués (et mal échappés)?
j'essaie de faire quelque chose comme ceci:
for file in `find . *.foo`
do
somecommand $file
done
mais la commande ne fonctionne pas car $file est très bizarre. Parce que mon arborescence de répertoires a des noms de fichiers merdiques (y compris des espaces), je dois échapper à la commande find
. Mais aucune des évasions évidentes ne semble fonctionner:
-ls
me donne les fragments de nom de fichier délimités par l'espace
-fprint
ne fait pas mieux.
j'ai aussi essayé: for file in "
trouver . *.foo-ls "; do echo $file; done
- but that gives all of the responses from find in one long line.
des indices? Je suis content de pouvoir contourner le problème, mais je suis frustré de ne pas pouvoir le découvrir.
Merci, Alex
(Salut Matt!)
6 réponses
Vous avez beaucoup de réponses qui expliquent bien comment le faire; mais, à cause de l'achèvement je vais répéter et d'ajouter:
xargs
n'est utile que pour une utilisation interactive (lorsque vous savez que tous vos noms de fichiers sont simples - pas d'espaces ou de guillemets) ou lorsqu'il est utilisé avec l'option -0
. Sinon, il va tout casser.
find
est un outil très utile; mettez l'utiliser pour Piper des noms de fichiers dans xargs
(même avec -0
) est plutôt alambiqué comme find
peut le faire tout seul avec soit -exec command {} \;
ou -exec command {} +
selon ce que vous voulez:
find /path -name 'pattern' -exec somecommand {} \;
find /path -name 'pattern' -exec somecommand {} +
le premier exécute somecommand
avec un argument pour chaque fichier récursivement dans /path
qui correspond à pattern
.
ce dernier exécute somecommand
avec autant d'arguments qu'il convient sur la ligne de commande à la fois pour les fichiers récursivement dans /path
qui correspondent à pattern
.
lequel utiliser dépend de somecommand
. S'il peut prendre plusieurs arguments de nom de fichier (comme rm
, grep
, etc.) alors cette dernière option est plus rapide (puisque vous utilisez somecommand
beaucoup moins souvent). Si somecommand
ne prend qu'un seul argument alors vous avez besoin de l'ancienne solution. Regardez la page de manuel de somecommand
.
plus sur find
: http://mywiki.wooledge.org/UsingFind
Dans bash
, for
est un énoncé qui effectue une itération sur arguments . Si vous faites quelque chose comme ceci:
for foo in "$bar"
vous donnez for
un argument d'itérer (notez les guillemets!). Si vous faites quelque chose comme ceci:
for foo in $bar
vous demandez bash
de prendre le le contenu de bar
et le déchirer partout où il y a des espaces, des onglets ou des lignes (techniquement, quels que soient les caractères dans IFS
) et utiliser les morceaux de cette opération comme arguments pour. ce n'est PAS les noms de fichiers . En supposant que le résultat d'une longue chaîne de caractères qui contient des noms de fichiers séparés partout où il y a des rendements d'espace dans un tas de noms de fichiers est tout simplement erroné. Comme vous l'avez remarqué.
la réponse est: Ne pas utiliser for
, c'est évidemment le mauvais outil. Les commandes find
ci-dessus supposent Toutes que somecommand
est un exécutable dans PATH
. Si c'est une instruction bash
, vous aurez besoin de cette construction à la place (itère sur la sortie find
, comme vous avez essayé, mais en toute sécurité):
while read -r -d ''; do
somebashstatement "$REPLY"
done < <(find /path -name 'pattern' -print0)
cela utilise une boucle while-read
qui lit des parties de la chaîne find
jusqu'à ce qu'elle atteigne un byte NULL
(ce qui est ce que -print0
utilise pour séparer les noms de fichiers). Comme les octets NULL
ne peuvent pas faire partie des noms de fichiers (contrairement aux espaces, aux onglets et aux lignes), c'est une opération sûre.
si vous n'avez pas besoin de somebashstatement
pour faire partie de votre script (par ex. il ne change pas l'environnement du script en gardant un compteur ou en définissant une variable ou un tel) alors vous pouvez toujours utiliser find
's -exec
pour exécuter votre bash
déclaration:
find /path -name 'pattern' -exec bash -c 'somebashstatement ""' -- {} \;
find /path -name 'pattern' -exec bash -c 'for file; do somebashstatement "$file"; done' -- {} +
ici, le -exec
exécute une commande bash
avec trois arguments ou plus.
- Le bash instruction à exécuter.
- Un
--
.bash
va mettre ça dans"1519510920"
, vous pouvez mettre ce que vous voulez ici, vraiment. - votre nom de fichier ou vos noms de fichier (selon que vous avez utilisé
{} \;
ou{} +
respectivement). Le nom de fichier(s) fin(s) dans(et
,
, ... si il n'y a plus d'un, bien sûr).
l'instruction bash
dans le premier find
commande ici exécute somebashstatement
avec le nom de fichier comme argument.
la déclaration bash
dans la deuxième commande find
exécute ici un for
( ! ) boucle qui itère sur chaque paramètre de position (c'est ce que la syntaxe réduite for
- for foo; do
- fait) et exécute un somebashstatement
avec le nom de fichier comme argument. La différence ici entre la toute première déclaration find
que j'ai montrée avec -exec {} +
est que nous n'exécutons qu'un seul processus bash
pour des lots de noms de fichiers mais encore un somebashstatement
pour chaque de ces noms de fichiers.
tout cela est également bien expliqué dans la page UsingFind
liée ci-dessus.
au lieu de compter sur le shell pour faire ce travail, comptez sur find pour le faire:
find . -name "*.foo" -exec somecommand "{}" \;
alors le nom du fichier sera correctement échappé, et jamais interprété par le shell.
find . -name '*.foo' -print0 | xargs -0 -n 1 somecommand
Il ne gênant si vous avez besoin d'exécuter un certain nombre de commandes shell sur chaque élément.
xargs est votre ami. Vous voudrez également étudier l'option -0 (zéro) avec elle. find
(avec -print0
) aidera à produire la liste. La page Wikipedia a quelques bons exemples.
une autre raison utile d'utiliser xargs
, est que si vous avez beaucoup de fichiers( des dizaines ou plus), xargs les divisera en appels individuels à n'importe quel xargs est alors appelé à exécuter (dans le premier exemple Wikipédia, rm
)
find . -name '*.foo' -print0 | xargs -0 sh -c 'for F in "${@}"; do ...; done' ""151900920""
j'ai dû faire quelque chose de similaire il y a quelque temps, renommer des fichiers pour leur permettre de vivre dans des environnements Win32:
#!/bin/bash
IFS=$'\n'
function RecurseDirs
{
for f in "$@"
do
newf=echo "${f}" | sed -e 's/[\/:\*\?#"\|<>]/_/g'
if [ ${newf} != ${f} ]; then
echo "${f}" "${newf}"
mv "${f}" "${newf}"
f="${newf}"
fi
if [[ -d "${f}" ]]; then
cd "${f}"
RecurseDirs $(ls -1 ".")
fi
done
cd ..
}
RecurseDirs .
c'est probablement un peu simpliste, ça n'évite pas les collisions de noms, et je suis sûr que ça pourrait être mieux fait -- mais ça élimine le besoin d'utiliser le nom de base sur les résultats de recherche (dans mon cas) avant d'effectuer mon remplacement sed.
je pourrais vous demander, Que faites-vous aux fichiers trouvés, exactement?