Je souhaite parcourir plusieurs répertoires et renommer tous les fichiers se terminant par _test.rb pour se terminer par _spec.rb. C'est quelque chose que je n'ai jamais vraiment compris comment faire avec bash, alors cette fois-ci, j'ai pensé que je ferais des efforts pour l'obtenir. Jusqu'ici, je me suis retrouvé à court, 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.
Lorsque je l'exécute, la sortie de chaque nom de fichier correspondant est la suivante:
mv original original
c'est-à-dire que la substitution par sed a été perdue. C'est quoi le truc?
Cela se produit parce que sed
reçoit la chaîne {}
comme entrée, ce qui peut être vérifié avec:
find . -exec echo `echo "{}" | sed 's/./foo/g'` \;
qui affiche foofoo
pour chaque fichier du répertoire, de manière récursive. La raison de ce comportement est que le pipeline est exécuté une fois, par le shell, lorsqu'il développe la commande entière.
Il n’existe aucun moyen de citer le pipeline sed
de telle manière que find
l’exécute pour chaque fichier, car find
n’exécute pas de commandes via le shell et n’a pas notion de pipelines ou de citations arrières. Le manuel GNU findutils) explique comment effectuer une tâche similaire en plaçant le pipeline dans un script Shell distinct:
#!/bin/sh
echo "$1" | 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 en une seule commande, mais je ne vais pas essayer.)
Pour le résoudre de la manière la plus proche du problème initial, utilisez probablement l'option "args par ligne de commande" de xargs:
find . -name *_test.rb | sed -e "p;s/test/spec/" | xargs -n2 mv
Il trouve les fichiers dans le répertoire de travail actuel de manière récursive, renvoie le nom de fichier d'origine (p
), puis un nom modifié (s/test/spec/
) et transmet le tout à mv
par paires (xargs -n2
). Attention, dans ce cas, le chemin lui-même ne devrait pas contenir de chaîne test
.
vous voudrez peut-être envisager 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 $0 ${0/test.rb/spec.rb}' {} \;
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 du lot que vous recherchez ...
En supposant que vous utilisiez bash
comme shell:
$ echo $Shell
/bin/bash
$ _
... et en supposant que vous ayez activé l'option dite globstar
Shell:
$ shopt -p globstar
shopt -s globstar
$ _
... et enfin en supposant que vous ayez installé l'utilitaire rename
(présent dans le fichier util-linux-ng
paquet)
$ which rename
/usr/bin/rename
$ _
... vous pouvez alors renommer le lot en bash one-liner comme suit:
$ rename _test _spec **/*_test.rb
(L’option globstar
Shell assurera que bash trouve tous les objets correspondants *_test.rb
fichiers, quelle que soit leur profondeur dans la hiérarchie des répertoires ... utilisez help shopt
pour savoir comment définir l'option)
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}
enlève suffix
de la valeur de var
.
ou, pour le faire en utilisant sed:
for i in `find -name '*_test.rb'` ; do mv $i `echo $i | sed 's/test/spec/'` ; done
Le moyen le plus simple:
find . -name "*_test.rb" | xargs rename s/_test/_spec/
Le moyen le plus rapide (en supposant que vous ayez 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 entraîne le dépassement de la longueur maximale autorisée pour la ligne de commande résultante.
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 combien RAM vous devez travailler; sinon, utilisez top
ou votre application de surveillance de l'activité des systèmes.
ne manière plus sûre (en supposant que vous ayez 1000000 octets de RAM avec lesquels travailler):
find . -name "*_test.rb" | xargs -s 1000000 rename s/_test/_spec/
Pour cela, vous n'avez pas besoin de sed
. Vous pouvez parfaitement vous retrouver seul avec une boucle while
alimentée avec le résultat de find
par un substitution de processus .
Donc, si vous avez une expression find
qui sélectionne les fichiers nécessaires, utilisez la syntaxe suivante:
while IFS= read -r file; do
echo "mv $file ${file%_test.rb}_spec.rb" # remove "echo" when OK!
done < <(find -name "*_test.rb")
Ceci va find
fichiers et les renommer tous en agrégeant la chaîne _test.rb
à partir de la fin et en ajoutant _spec.rb
.
Pour cette étape, nous utilisons Shell Parameter Expansion où ${var%string}
supprime le motif correspondant le plus court "chaîne" 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
Voici ce qui a fonctionné pour moi lorsque les noms de fichiers contenaient des espaces. L'exemple ci-dessous renomme récursivement tous les fichiers .dar en fichiers .Zip:
find . -name "*.dar" -exec bash -c 'mv "$0" "`echo \"$0\" | sed s/.dar/.Zip/`"' {} \;
Ceci est un exemple qui devrait fonctionner dans tous les cas. Fonctionne recursiveley, Besoin seulement de Shell et supporte les noms de fichiers avec des espaces.
find spec -name "*_test.rb" -print0 | while read -d $'\0' file; do mv "$file" "`echo $file | sed s/test/spec/`"; done
Je n'ai pas le courage de tout refaire, mais j'ai écrit ceci en réponse à Commandline Find Sed Exec . Là, le demandeur voulait savoir comment se déplacer. une arborescence complète, excluant éventuellement un ou deux répertoires, et renommez tous les fichiers et répertoires contenant la chaîne "VIEILLE" afin qu'elle contienne "NEW".
Outre décrivant le comment avec une verbosité laborieuse ci-dessous, cette méthode peut également être unique en ce sens qu'elle intègre le débogage intégré. . En principe, il ne fait rien comme écrit, sauf compiler et enregistrer dans une variable toutes les commandes qu’il pense devoir exécuter pour effectuer le travail demandé.
Cela évite aussi explicitement les boucles autant que possible. En plus de la recherche récursive sed
pour plus d'une correspondance du motif, il n'y a pas d'autre récursion à ma connaissance.
Enfin, ceci est entièrement délimité par null
- il ne fait trébucher aucun caractère dans aucun nom de fichier, à l'exception du null
. Je ne pense pas que tu devrais avoir ça.
À propos, c'est [~ # ~] vraiment [~ # ~] rapide. Regardez:
% _mvnfind() { mv -n "${1}" "${2}" && cd "${2}"
> read -r SED <<SED
> :;s|${3}\(.*/[^/]*${5}\)|${4}\1|;t;:;s|\(${5}.*\)${3}|\1${4}|;t;s|^[0-9]*[\t]\(mv.*\)${5}|\1|p
> SED
> find . -name "*${3}*" -printf "%d\tmv %P ${5} %P\000" |
> sort -zg | sed -nz ${SED} | read -r ${6}
> echo <<EOF
> Prepared commands saved in variable: ${6}
> To view do: printf ${6} | tr "\000" "\n"
> To run do: sh <<EORUN
> $(printf ${6} | tr "\000" "\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 "\000" "\n" \
> | wc - ; echo ${sh_io} | tr "\000" "\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
REMARQUE: Le function
ci-dessus nécessitera probablement les versions GNU
de sed
et de find
pour gérer correctement les appels find printf
et sed -z -e
et :;recursive regex test;t
. Si ces options ne sont pas disponibles, la fonctionnalité peut probablement être dupliquée avec quelques ajustements mineurs.
Cela devrait faire tout ce que vous vouliez du début à la fin avec très peu de tracas. J'ai fait fork
avec sed
, mais je pratiquais aussi certaines techniques de ramification récursive sed
, c'est pourquoi je suis ici. C'est un peu comme avoir une coupe de cheveux à prix réduit dans une école de coiffure, je suppose. Voici le flux de travail:
rm -rf ${UNNECESSARY}
./app
Peut être indésirable. Supprimez-le ou déplacez-le ailleurs auparavant, ou vous pouvez également créer une routine \( -path PATTERN -exec rm -rf \{\} \)
vers find
pour le faire par programme, mais c'est tout à vous._mvnfind "${@}"
${sh_io}
Est particulièrement important car il enregistre le retour de la fonction. ${sed_sep}
Arrive en deuxième position; c'est une chaîne arbitraire utilisée pour référencer la récurrence de sed
dans la fonction. Si ${sed_sep}
Est défini sur une valeur susceptible d'être trouvée dans l'un des noms de chemin ou de fichier sur lesquels vous avez agi ... eh bien, ne le laissez pas être ainsi.mv -n $1 $2
-noclobber
Définie pour mv
; comme écrit, cette fonction ne mettra pas ${SRC_DIR}
là où un ${TGT_DIR}
existe déjà.read -R SED <<HEREDOC
find . -name ${OLD} -printf
find
. Avec find
, nous recherchons uniquement tout ce qui doit être renommé car nous avons déjà effectué toutes les opérations place-to-place mv
avec la première commande de la fonction. Plutôt que de prendre une action directe avec find
, comme un appel exec
, par exemple, nous l’utilisons pour construire la ligne de commande de manière dynamique avec -printf
.%dir-depth :tab: 'mv '%path-to-${SRC}' '${sed_sep}'%path-again :null delimiter:'
find
a localisé les fichiers dont nous avons besoin, il est directement construit et imprimé ( la plupart) de la commande dont nous aurons besoin pour traiter votre changement de nom. Le %dir-depth
Ajouté au début de chaque ligne vous aidera à vous assurer que nous n'essayons pas de renommer un fichier ou un répertoire dans l'arborescence avec un objet parent qui n'a pas encore été renommé. find
utilise toutes sortes de techniques d'optimisation pour parcourir votre arborescence de système de fichiers et il n'est pas certain que les données dont nous avons besoin soient renvoyées dans un ordre sécurisé pour les opérations. C'est pourquoi nous avons ensuite ...sort -general-numerical -zero-delimited
find
en fonction de %directory-depth
Afin que les chemins les plus proches de $ {SRC} soient travaillés en premier. Cela évite les erreurs possibles impliquant des fichiers mv
dans des emplacements inexistants et minimise le besoin de bouclage récursif. ( en fait, vous pourriez avoir du mal à trouver une boucle du tout)sed -ex :rcrs;srch|(save${sep}*til)${OLD}|\saved${SUBSTNEW}|;til ${OLD=0}
%Path
Imprimé pour chaque chaîne, au cas où elle contient plus d’une valeur $ {OLD} à remplacer. Toutes les autres solutions que j'ai imaginées impliquaient un deuxième processus sed
et, bien qu'une boucle courte ne soit peut-être pas souhaitable, elle vaut certainement mieux que de générer et de mettre en place un processus complet.sed
fait ici est une recherche de $ {sed_sep}. Après l'avoir trouvé, il enregistre tous les caractères rencontrés jusqu'à ce qu'il trouve $ {OLD}, qu'il remplace ensuite par $ {NEW}. Il revient ensuite à $ {sed_sep} et cherche à nouveau $ {OLD}, au cas où cela se produirait plus d'une fois dans la chaîne. S'il n'est pas trouvé, il affiche la chaîne modifiée dans stdout
(qu'il reprend ensuite) et termine la boucle.mv
, qui doit inclure $ {OLD} bien sûr, l'inclut et que la seconde moitié est modifiée autant de fois comme il est nécessaire d'effacer le nom $ {OLD} du chemin de destination de mv
.sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
-exec
Effectués ici se font sans deuxième fork
. Comme nous l'avons vu précédemment, nous modifions la commande mv
fournie par la commande de fonction find
de -printf
Afin de modifier correctement toutes les références de $ {OLD } à $ {NEW}, mais pour ce faire, nous avons dû utiliser des points de référence arbitraires qui ne devraient pas être inclus dans la sortie finale. Donc, une fois que sed
a terminé, il lui est demandé d'effacer ses points de référence du tampon de maintien avant de le transmettre.ET MAINTENANT, NOUS SOMMES DE RETOUR
read
recevra une commande qui ressemble à ceci:
% mv /path2/$SRC/$OLD_DIR/$OLD_FILE /same/path_w/$NEW_DIR/$NEW_FILE \000
Il sera read
dans ${msg}
En tant que ${sh_io}
Qui peut être examiné à volonté en dehors de la fonction.
Cool.
-Mike
Dans la réponse de ramtam que j'aime bien, la partie recherche fonctionne correctement, mais pas le reste si le chemin contient des espaces. Je ne connais pas trop sed, mais j'ai pu modifier cette réponse en:
find . -name "*_test.rb" | Perl -pe 's/^((.*_)test.rb)$/"\1" "\2spec.rb"/' | xargs -n2 mv
J'ai vraiment besoin d'un changement comme celui-ci car dans mon cas d'utilisation, la commande finale ressemble davantage à
find . -name "olddir" | Perl -pe 's/^((.*)olddir)$/"\1" "\2new directory"/' | xargs -n2 mv
J'ai pu gérer les noms de fichiers avec des espaces en suivant les exemples suggérés par onitake.
Ceci ne ne se rompt pas si le chemin contient des espaces ou la chaîne test
:
find . -name "*_test.rb" -print0 | while read -d $'\0' file
do
echo mv "$file" "$(echo $file | sed s/test/spec/)"
done
si vous avez Ruby (1.9+)
Ruby -e 'Dir["**/*._test.rb"].each{|x|test(?f,x) and File.rename(x,x.gsub(/_test/,"_spec") ) }'
Voici un joli oneliner qui fait l'affaire. Sed ne peut pas gérer ce droit, surtout si xargs est passé avec plusieurs variables avec -n 2. Une sous-station bash gérera cela facilement comme ceci:
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, -print 0 gérera les espaces vides dans les chemins.
$ 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
Une manière plus sécurisée de renommer avec find utils et le type d’expression régulière sed:
mkdir ~/practice
cd ~/practice
touch classic.txt.txt
touch folk.txt.txt
Supprimez l'extension ".txt.txt" comme suit -
cd ~/practice
find . -name "*txt" -execdir sh -c 'mv "$0" `echo "$0" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'`' {} \;
Si vous utilisez le + à la place de; afin de fonctionner en mode de traitement par lots, la commande ci-dessus ne renommera que le premier fichier correspondant, mais pas la liste complète des correspondances de fichiers par 'trouver'.
find . -name "*txt" -execdir sh -c 'mv "$0" `echo "$0" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'`' {} +
Votre question semble concerner sed, mais pour atteindre votre objectif de changement de nom récursif, je suggérerais ce qui suit, déchirée sans vergogne d’une autre réponse que j’ai donnée ici: changement de nom récursif dans bash
#!/bin/bash
IFS=$'\n'
function RecurseDirs
{
for f in "$@"
do
newf=echo "${f}" | sed -e 's/^(.*_)test.rb$/\1spec.rb/g'
echo "${f}" "${newf}"
mv "${f}" "${newf}"
f="${newf}"
if [[ -d "${f}" ]]; then
cd "${f}"
RecurseDirs $(ls -1 ".")
fi
done
cd ..
}
RecurseDirs .
Je partage ce post car il est un peu lié à la question. Désolé de ne pas fournir plus de détails. J'espère que ça aide quelqu'un d'autre. http://www.peteryu.ca/tutorials/shellscripting/batch_rename