web-dev-qa-db-fra.com

Renommer récursivement des fichiers en utilisant find et sed

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?

77
opsb

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

32
Fred Foo

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.

113
ramtam

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
23
ajreal

Je trouve celui-ci plus court

find . -name '*_test.rb' -exec bash -c 'echo mv $0 ${0/test.rb/spec.rb}' {} \;
17
csg

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)

9
pvandenberk

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
9
Wayne Conrad

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/
6
l3x

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${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
2
fedorqui

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/`"' {} \;
2
rskengineer

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
1
eldy

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}
    • J'ai volontairement laissé de côté tout appel fonctionnel susceptible de supprimer ou de détruire des données de toute nature. Vous mentionnez que ./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 "${@}"
    • Déclarez ses arguments et appelez la fonction worker. ${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
    • L'arbre entier est déplacé depuis le début. Cela évitera beaucoup de maux de tête; Crois moi. Le reste de ce que vous voulez faire - le changement de nom - est simplement une question de métadonnées du système de fichiers. Si, par exemple, vous déplaciez cela d'un lecteur à un autre ou à travers les limites d'un système de fichiers, vous feriez mieux de le faire en une fois avec une seule commande. C'est aussi plus sûr. Notez l’option -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
    • J'ai localisé toutes les commandes de sed ici pour économiser sur les tracas de sortie et les lire dans une variable pour alimenter sed en-dessous. Explication ci-dessous.
  • find . -name ${OLD} -printf
    • Nous commençons le processus 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:'
    • Une fois que 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
    • Nous trions toutes les sorties de 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}
    • Je pense que c’est la seule boucle de l’ensemble du script, qui ne boucle que sur le deuxième %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.
    • Donc, fondamentalement, ce que 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.
    • Cela évite d'avoir à analyser la chaîne entière et garantit que la première moitié de la chaîne de commande 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
    • Les deux appels -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

1
mikeserv

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
1
dzs0000

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
1
James

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
kurumi

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.

0
Orsiris de Jong
$ 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
Damodharan R

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:]]+$//'`' {} +
0
Sathish

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 .
0
dreynold

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

0
Breton F.