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?

73
demandé sur opsb 2011-01-25 16:08:51

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.)

31
répondu Fred Foo 2011-01-25 15:17:58

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 .

105
répondu ramtam 2012-07-31 17:42:00

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
22
répondu ajreal 2011-01-25 19:29:49

je trouve celui-ci plus court

find . -name '*_test.rb' -exec bash -c 'echo mv "151900920" ${0/test.rb/spec.rb}' {} \;
17
répondu csg 2011-10-16 16:27:49

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
9
répondu Wayne Conrad 2011-01-25 15:48:04

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)

9
répondu pvandenberk 2011-01-27 15:29:26

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/
5
répondu l3x 2015-05-13 18:21:43

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 ${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
2
répondu fedorqui 2017-05-23 12:34:22

si vous avez Ruby (1.9+)

ruby -e 'Dir["**/*._test.rb"].each{|x|test(?f,x) and File.rename(x,x.gsub(/_test/,"_spec") ) }'
1
répondu kurumi 2011-01-25 15:08:34

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
1
répondu dzs0000 2012-07-31 18:20:49

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.
  • _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 de sed 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.
  • 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 pour mv ; comme écrit, cette fonction ne mettra pas ${SRC_DIR} où un ${TGT_DIR} existe déjà.
  • 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 . Avec find nous ne cherchons que tout ce qui doit être renommé car nous avons déjà fait toutes les opérations mv avec le premier commandement 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 dynamiquement avec -printf .
  • %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...
  • 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 impliquant mv 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 )
  • 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 processus sed , 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 en stdout (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 de mv .
  • sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
    • les deux -exec les appels ici se produisent sans un second fork . Dans le premier, comme nous l'avons vu, nous modifions la commande mv comme suit: fourni par la commande de fonction find '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 que sed 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.

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

1
répondu mikeserv 2017-05-23 12:18:03

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
1
répondu James 2014-12-03 23:04:57

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
1
répondu eldy 2015-09-28 09:33:04

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/`"' {} \;
1
répondu rskengineer 2016-07-07 20:09:11
$ 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
0
répondu Damodharan R 2011-01-25 14:13:06

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 .
0
répondu dreynold 2017-05-23 11:54:36

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:]]+$//'`' {} +
0
répondu Sathish 2014-10-23 06:15:52

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.

0
répondu deajan 2015-10-02 19:57:18