Renommer recursivement les fichiers en utilisant find Et sed
je veux parcourir un tas de répertoires et renommer tous les fichiers qui se terminent par _test.rb à la fin dans _spec.rb à la place. C'est quelque chose que je n'ai jamais tout à fait compris comment faire avec bash alors cette fois, j'ai pensé que je vais mettre un peu d'effort pour le clouer. Jusqu'à présent, je n'ai pas été à la hauteur, cependant, mon meilleur effort est:
find spec -name "*_test.rb" -exec echo mv {} `echo {} | sed s/test/spec/` ;
NB: il y a un écho supplémentaire après exec pour que la commande soit imprimée au lieu d'être exécutée pendant que je la teste.
quand je l'exécute la sortie pour chaque nom de fichier apparié est:
mv original original
, c'est-à-dire que la substitution par sed a été perdue. Quel est le truc?
18 réponses
cela se produit parce que sed
reçoit la chaîne {}
comme entrée, comme peut être vérifié avec:
find . -exec echo `echo "{}" | sed 's/./foo/g'` \;
qui imprime foofoo
pour chaque fichier du répertoire, de façon récursive. La raison de ce comportement est que le pipeline est exécuté une fois, par le shell, quand il étend la commande entière.
il n'y a aucun moyen de citer le pipeline sed
de telle manière que find
l'exécutera pour chaque fichier, depuis find
n'exécute pas de commandes via le shell et n'a aucune notion de pipelines ou de backquotes. Le manuel GNU findutils explique comment exécuter une tâche similaire en mettant le pipeline dans un script shell séparé:
#!/bin/sh
echo "" | sed 's/_test.rb$/_spec.rb/'
(il peut y avoir une façon perverse d'utiliser sh -c
et une tonne de citations pour faire tout cela dans une commande, mais je ne vais pas essayer.)
pour le résoudre de la manière la plus proche du problème original serait probablement en utilisant xargs "args par ligne de commande" option:
find . -name *_test.rb | sed -e "p;s/test/spec/" | xargs -n2 mv
il trouve les fichiers dans le répertoire de travail courant de façon récursive, fait écho au nom de fichier original ( p
) et puis un nom modifié ( s/test/spec/
) et alimente tout à mv
par paires ( xargs -n2
). Attention, dans ce cas, le chemin lui-même ne devrait pas contenir de chaîne de caractères test
.
vous pourriez vouloir considérer une autre façon comme
for file in $(find . -name "*_test.rb")
do
echo mv $file `echo $file | sed s/_test.rb$/_spec.rb/`
done
je trouve celui-ci plus court
find . -name '*_test.rb' -exec bash -c 'echo mv "151900920" ${0/test.rb/spec.rb}' {} \;
Vous pouvez le faire sans sed, si vous voulez:
for i in `find -name '*_test.rb'` ; do mv $i ${i%%_test.rb}_spec.rb ; done
${var%%suffix}
bandes suffix
à partir de la valeur de var
.
ou, le faire à l'aide de sed:
for i in `find -name '*_test.rb'` ; do mv $i `echo $i | sed 's/test/spec/'` ; done
vous mentionnez que vous utilisez bash
comme shell, auquel cas vous n'avez pas besoin de find
et sed
pour obtenir le changement de nom de lot que vous recherchez...
en supposant que vous utilisez bash
comme shell:
$ echo $SHELL
/bin/bash
$ _
... et si vous avez activé l'option globstar
shell:
$ shopt -p globstar
shopt -s globstar
$ _
... et enfin en supposant que vous avez installé le rename
utilitaire (qui se trouve dans la util-linux-ng
package)
$ which rename
/usr/bin/rename
$ _
... ensuite, vous pouvez obtenir le lot de renommage dans un bash one-liner comme suit:
$ rename _test _spec **/*_test.rb
(l'option shell globstar
assurera que bash trouve tous les fichiers *_test.rb
correspondants, peu importe à quel point ils sont imbriqués dans la hiérarchie des répertoires... utiliser help shopt
pour savoir comment définir l'option)
la voie la plus facile :
find . -name "*_test.rb" | xargs rename s/_test/_spec/
le moyen Le plus rapide (en supposant que vous avez 4 processeurs):
find . -name "*_test.rb" | xargs -P 4 rename s/_test/_spec/
si vous avez un grand nombre de fichiers à traiter, il est possible que la liste des noms de fichiers acheminés vers xargs fasse en sorte que la ligne de commande résultante dépasse la longueur maximale permise.
vous pouvez vérifier la limite de votre système en utilisant getconf ARG_MAX
sur la plupart des systèmes linux, vous pouvez utiliser free -b
ou cat /proc/meminfo
pour trouver la quantité de mémoire vive avec laquelle vous devez travailler; sinon, utilisez top
ou votre application de moniteur d'activité des systèmes.
une façon plus sûre (en supposant que vous avez 1000000 octets de ram pour travailler avec):
find . -name "*_test.rb" | xargs -s 1000000 rename s/_test/_spec/
pour cela vous n'avez pas besoin de sed
. Vous pouvez parfaitement obtenir seul avec une boucle while
alimentée avec le résultat de find
à travers un substitution de processus .
donc si vous avez une expression find
qui sélectionne les fichiers nécessaires, utilisez la syntaxe:
while IFS= read -r file; do
echo "mv $file ${file%_test.rb}_spec.rb" # remove "echo" when OK!
done < <(find -name "*_test.rb")
ce sera find
fichiers et renommer tous en rayant la chaîne de caractères _test.rb
de la fin et en ajoutant _spec.rb
.
pour cette étape, nous utilisons Shell Parameter Expansion où ${var%string}
supprime la chaîne de caractères la plus courte" string "de $var
.
$ file="HELLOa_test.rbBYE_test.rb"
$ echo "${file%_test.rb}" # remove _test.rb from the end
HELLOa_test.rbBYE
$ echo "${file%_test.rb}_spec.rb" # remove _test.rb and append _spec.rb
HELLOa_test.rbBYE_spec.rb
voir un exemple:
$ tree
.
├── ab_testArb
├── a_test.rb
├── a_test.rb_test.rb
├── b_test.rb
├── c_test.hello
├── c_test.rb
└── mydir
└── d_test.rb
$ while IFS= read -r file; do echo "mv $file ${file/_test.rb/_spec.rb}"; done < <(find -name "*_test.rb")
mv ./b_test.rb ./b_spec.rb
mv ./mydir/d_test.rb ./mydir/d_spec.rb
mv ./a_test.rb ./a_spec.rb
mv ./c_test.rb ./c_spec.rb
si vous avez Ruby (1.9+)
ruby -e 'Dir["**/*._test.rb"].each{|x|test(?f,x) and File.rename(x,x.gsub(/_test/,"_spec") ) }'
dans la réponse de ramtam que j'aime bien, la partie find fonctionne bien mais le reste ne fonctionne pas si le chemin a des espaces. Je ne suis pas très familier avec sed, mais j'ai pu modifier cette réponse à:
find . -name "*_test.rb" | perl -pe 's/^((.*_)test.rb)$/"" "spec.rb"/' | xargs -n2 mv
j'avais vraiment besoin d'un changement comme celui-ci parce que dans mon cas d'utilisation la commande finale ressemble plus à
find . -name "olddir" | perl -pe 's/^((.*)olddir)$/"" "new directory"/' | xargs -n2 mv
Je n'ai pas le cœur de recommencer, mais j'ai écrit ceci en réponse à Ligne De Commande trouver sed Exec . Là, l'asker a voulu savoir comment déplacer un arbre entier, éventuellement en excluant un ou deux répertoires, et renommer tous les fichiers et répertoires contenant la chaîne de caractères "OLD à la place de contenir " NEW .
en plus de décrivant le comment avec verbosité minutieuse ci-dessous, cette méthode peut également être unique en ce sens qu'elle intègre le débogage intégré. Fondamentalement, il ne fait rien du tout tel qu'il est écrit, sauf compiler et sauvegarder dans une variable toutes les commandes qu'il croit devoir faire pour effectuer le travail demandé.
elle aussi explicitement évite les boucles autant que possible. Outre le sed
recherche récursive pour plus d'une correspondance du modèle il n'y a pas d'autre récursion à ma connaissance.
et enfin, c'est entièrement null
délimité - il ne trébuche sur aucun caractère dans aucun nom de fichier sauf le null
. Je ne pense pas que vous devriez avoir.
soit dit en passant, c'est vraiment fast. Regardez:
% _mvnfind() { mv -n "" "" && cd ""
> read -r SED <<SED
> :;s|\(.*/[^/]*\)||;t;:;s|\(.*\)||;t;s|^[0-9]*[\t]\(mv.*\)||p
> SED
> find . -name "**" -printf "%d\tmv %P %P"151900920"0" |
> sort -zg | sed -nz ${SED} | read -r
> echo <<EOF
> Prepared commands saved in variable:
> To view do: printf | tr ""151900920"0" "\n"
> To run do: sh <<EORUN
> $(printf | tr ""151900920"0" "\n")
> EORUN
> EOF
> }
% rm -rf "${UNNECESSARY:=/any/dirs/you/dont/want/moved}"
% time ( _mvnfind ${SRC=./test_tree} ${TGT=./mv_tree} \
> ${OLD=google} ${NEW=replacement_word} ${sed_sep=SsEeDd} \
> ${sh_io:=sh_io} ; printf %b\000 "${sh_io}" | tr ""151900920"0" "\n" \
> | wc - ; echo ${sh_io} | tr ""151900920"0" "\n" | tail -n 2 )
<actual process time used:>
0.06s user 0.03s system 106% cpu 0.090 total
<output from wc:>
Lines Words Bytes
115 362 20691 -
<output from tail:>
mv .config/replacement_word-chrome-beta/Default/.../googlestars \
.config/replacement_word-chrome-beta/Default/.../replacement_wordstars
NOTE: ci-dessus function
exigera probablement des GNU
versions de sed
et find
pour traiter correctement les appels find printf
et sed -z -e
et :;recursive regex test;t
. Si elles ne sont pas à votre disposition les fonctionnalités peuvent être dupliqués avec quelques ajustements mineurs.
cela devrait faire tout ce que vous vouliez du début à la fin avec très peu de bruit. J'ai fait fork
avec sed
, mais je pratiquais aussi quelques sed
techniques de ramification récursive donc c'est pourquoi je suis ici. C'est un peu comme arriver une coupe de cheveux à rabais dans une école de barbier, je suppose. Voici le workflow:
-
rm -rf ${UNNECESSARY}
- j'ai intentionnellement omis tout appel fonctionnel qui pourrait supprimer ou détruire des données de toute nature. Vous mentionnez que
./app
pourrait être indésirable. Supprimez-le ou déplacez-le Ailleurs à l'avance, ou, alternativement, vous pouvez construire un\( -path PATTERN -exec rm -rf \{\} \)
routine àfind
pour le faire programmatiquement, mais celui-ci est tout à vous.
- j'ai intentionnellement omis tout appel fonctionnel qui pourrait supprimer ou détruire des données de toute nature. Vous mentionnez que
-
_mvnfind "${@}"
- déclare ses arguments et appelle la fonction worker.
${sh_io}
est particulièrement important en ce qu'il enregistre le retour de la fonction.${sed_sep}
arrive en seconde; c'est une chaîne arbitraire utilisée pour faire référence à la récursion desed
dans la fonction. Si${sed_sep}
est défini à une valeur qui pourrait potentiellement être trouvée dans l'un de vos noms de chemin ou de fichier sur lesquels vous avez agi... eh bien, il suffit de ne pas le laisser être.
- déclare ses arguments et appelle la fonction worker.
-
mv -n
- L'arbre entier est déplacé depuis le début. Cela évitera beaucoup de maux de tête; croyez-moi. Le reste de ce que vous voulez faire - le renommage - est simplement une question de métadonnées du système de fichiers. Si, par exemple, vous déplaciez ceci d'un disque à un autre, ou si vous dépassiez les limites du système de fichiers, vous feriez mieux de le faire immédiatement avec une commande. Il est également plus sûr. Note l'option
-noclobber
défini pourmv
; comme écrit, cette fonction ne mettra pas${SRC_DIR}
où un${TGT_DIR}
existe déjà.
- L'arbre entier est déplacé depuis le début. Cela évitera beaucoup de maux de tête; croyez-moi. Le reste de ce que vous voulez faire - le renommage - est simplement une question de métadonnées du système de fichiers. Si, par exemple, vous déplaciez ceci d'un disque à un autre, ou si vous dépassiez les limites du système de fichiers, vous feriez mieux de le faire immédiatement avec une commande. Il est également plus sûr. Note l'option
-
read -R SED <<HEREDOC
- j'ai localisé toutes les commandes de sed ici pour sauver sur les hassles échappant et les lire dans une variable pour alimenter à sed ci-dessous. L'explication ci-dessous.
-
find . -name ${OLD} -printf
- nous entamons le processus
find
. Avecfind
nous ne cherchons que tout ce qui doit être renommé car nous avons déjà fait toutes les opérationsmv
avec le premier commandement de la fonction. Plutôt que de prendre une action directe avecfind
, comme un appelexec
, par exemple, nous l'utilisons pour construire la ligne de commande dynamiquement avec-printf
.
- nous entamons le processus
-
%dir-depth :tab: 'mv '%path-to-${SRC}' '${sed_sep}'%path-again :null delimiter:'
- après
find
localise les fichiers que nous besoin il construit et imprime directement ( most ) de la commande nous aurons besoin de traiter votre renommage. Le%dir-depth
placé au début de chaque ligne aidera à s'assurer que nous n'essayons pas de renommer un fichier ou un répertoire dans l'arbre avec un objet parent qui n'a pas encore été renommé.find
utilise toutes sortes de techniques d'optimisation pour parcourir votre arborescence du système de fichiers et il n'est pas sûr qu'il retournera les données dont nous avons besoin dans un ordre de sécurité pour les opérations. Ce est pourquoi nous suivant...
- après
-
sort -general-numerical -zero-delimited
- nous trions toutes les sorties de
find
basées sur%directory-depth
de sorte que les chemins Les plus proches en relation avec ${SRC} soient travaillés en premier. Cela évite les erreurs possibles impliquantmv
ing fichiers dans des endroits inexistants, et il minimise la nécessité de boucle récursive. ( en fait, vous pourriez être difficile de trouver une boucle du tout )
- nous trions toutes les sorties de
-
sed -ex :rcrs;srch|(save${sep}*til)${OLD}|\saved${SUBSTNEW}|;til ${OLD=0}
- je pense que c'est la seule boucle dans l'ensemble du script, et il ne Boucle que sur le second
%Path
imprimé pour chaque chaîne au cas où il contient plus d'une valeur ${ancienne} qui pourrait avoir besoin de remplacement. Toutes les autres solutions que j'ai imaginées impliquaient un deuxième processussed
, et bien qu'une courte boucle ne soit pas souhaitable, certainement il bat fraie et fourche un processus entier. - So fondamentalement, ce que
sed
fait ici est de rechercher ${sed_sep}, puis, l'ayant trouvé, le sauve et tous les caractères qu'il rencontre jusqu'à ce qu'il trouve ${OLD}, qu'il remplace ensuite par ${NEW}. Il retourne ensuite à $ {sed_sep} et cherche à nouveau ${OLD}, au cas où il se produirait plus d'une fois dans la chaîne. S'il n'est pas trouvé, il imprime la chaîne modifiée enstdout
(qu'il attrape ensuite à nouveau) et termine la boucle. - cela évite d'avoir à analyser la chaîne entière, et s'assure que la première moitié de la chaîne de commande
mv
, qui doit inclure ${ancien} bien sûr, l'inclut, et la seconde moitié est modifiée autant de fois qu'il est nécessaire pour effacer le nom ${ancien} du chemin de destination demv
.
- je pense que c'est la seule boucle dans l'ensemble du script, et il ne Boucle que sur le second
-
sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
- les deux
-exec
les appels ici se produisent sans un secondfork
. Dans le premier, comme nous l'avons vu, nous modifions la commandemv
comme suit: fourni par la commande de fonctionfind
's-printf
comme nécessaire pour modifier correctement toutes les références de ${Ancien} à ${nouveau}, mais pour ce faire nous avons dû utiliser quelques points de référence arbitraires qui ne devraient pas être inclus dans la sortie finale. Ainsi, une fois quesed
a terminé tout ce qu'il doit faire, nous lui demandons d'effacer ses points de référence du hold-buffer avant de le passer.
- les deux
et MAINTENANT NOUS SOMMES DE RETOUR VERS
read
recevra une commande qui ressemble à ceci:
% mv /path2/$SRC/$OLD_DIR/$OLD_FILE /same/path_w/$NEW_DIR/$NEW_FILE "151910920"0
il sera read
il dans ${msg}
comme ${sh_io}
qui peut être examiné à volonté en dehors de la fonction.
Cool.
- Mike
j'ai pu gérer les noms de fichiers avec des espaces en suivant les exemples suggérés par onitake.
Ce n'est pas pause si le chemin contient des espaces ou la chaîne test
:
find . -name "*_test.rb" -print0 | while read -d $'"151900920"' file
do
echo mv "$file" "$(echo $file | sed s/test/spec/)"
done
C'est un exemple qui devrait fonctionner dans tous les cas. Fonctionne recursiveley, besoin juste shell, et le soutien des noms de fichiers avec des espaces.
find spec -name "*_test.rb" -print0 | while read -d $'"151900920"' file; do mv "$file" "`echo $file | sed s/test/spec/`"; done
voici ce qui a fonctionné pour moi lorsque les noms de fichiers avaient des espaces. L'exemple ci-dessous renomme tout de façon récursive .dar fichiers .fichiers zip:
find . -name "*.dar" -exec bash -c 'mv ""151900920"" "`echo \""151900920"\" | sed s/.dar/.zip/`"' {} \;
$ find spec -name "*_test.rb"
spec/dir2/a_test.rb
spec/dir1/a_test.rb
$ find spec -name "*_test.rb" | xargs -n 1 /usr/bin/perl -e '($new=$ARGV[0]) =~ s/test/spec/; system(qq(mv),qq(-v), $ARGV[0], $new);'
`spec/dir2/a_test.rb' -> `spec/dir2/a_spec.rb'
`spec/dir1/a_test.rb' -> `spec/dir1/a_spec.rb'
$ find spec -name "*_spec.rb"
spec/dir2/b_spec.rb
spec/dir2/a_spec.rb
spec/dir1/a_spec.rb
spec/dir1/c_spec.rb
votre question semble être au sujet de sed, mais pour accomplir votre but de renommer récursive, je suggérerais la suivante, sans vergogne arraché d'une autre réponse que j'ai donnée ici: rename récursive dans bash
#!/bin/bash
IFS=$'\n'
function RecurseDirs
{
for f in "$@"
do
newf=echo "${f}" | sed -e 's/^(.*_)test.rb$/spec.rb/g'
echo "${f}" "${newf}"
mv "${f}" "${newf}"
f="${newf}"
if [[ -d "${f}" ]]; then
cd "${f}"
RecurseDirs $(ls -1 ".")
fi
done
cd ..
}
RecurseDirs .
moyen Plus sûr de faire de renommer avec trouver utils et sed expression régulière du type:
mkdir ~/practice
cd ~/practice
touch classic.txt.txt
touch folk.txt.txt
supprimer le ".txt.txt "extension comme suit -
cd ~/practice
find . -name "*txt" -execdir sh -c 'mv ""151910920"" `echo ""151910920"" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'`' {} \;
si vous utilisez le + à la place de ; afin de travailler en mode batch, la commande ci-dessus ne renommera que le premier fichier correspondant, mais pas la liste complète des fichiers correspondants par 'find'.
find . -name "*txt" -execdir sh -c 'mv ""151920920"" `echo ""151920920"" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'`' {} +
voici un joli oneliner qui fait l'affaire. Sed ne peut pas gérer cela correctement, surtout si plusieurs variables sont passées par xargs avec-n 2. Une substition de bash traiterait cela facilement comme:
find ./spec -type f -name "*_test.rb" -print0 | xargs -0 -I {} sh -c 'export file={}; mv $file ${file/_test.rb/_spec.rb}'
ajouter-type-f limitera les opérations de déplacement aux fichiers seulement, - print 0 gérera les espaces vides dans les chemins.