Frage Rekursive Umbenennung von Dateien mit find und sed


Ich möchte eine Reihe von Verzeichnissen durchsuchen und alle Dateien, die in _test.rb enden, umbenennen, um stattdessen in _spec.rb zu enden. Es ist etwas, bei dem ich noch nie herausgefunden habe, wie man mit bash vorgeht, also dachte ich diesmal, ich würde mich etwas anstrengen, um es zu knacken. Bisher bin ich allerdings kurz gekommen, mein größter Einsatz ist:

find spec -name "*_test.rb" -exec echo mv {} `echo {} | sed s/test/spec/` \;

NB: Es gibt ein zusätzliches Echo nach der Ausführung, so dass der Befehl gedruckt wird, anstatt zu laufen, während ich es teste. 

Wenn ich es ausführe, ist die Ausgabe für jeden übereinstimmenden Dateinamen:

mv original original

d.h. die Substitution durch sed ist verloren gegangen. Was ist der Trick?


72
2018-01-25 13:08


Ursprung


Antworten:


Dies geschieht, weil sed empfängt die Zeichenfolge {} als Eingabe, wie verifiziert werden kann mit:

find . -exec echo `echo "{}" | sed 's/./foo/g'` \;

welches druckt foofoo für jede Datei im Verzeichnis rekursiv. Der Grund für dieses Verhalten ist, dass die Pipeline einmal von der Shell ausgeführt wird, wenn sie den gesamten Befehl erweitert.

Es gibt keine Möglichkeit, das zu zitieren sed Pipeline so, dass find wird es für jede Datei ausführen, seit find führt Befehle nicht über die Shell aus und hat keine Ahnung von Pipelines oder Backquotes. Das GNU findutils-Handbuch erklärt, wie eine ähnliche Aufgabe ausgeführt wird, indem die Pipeline in ein separates Shell-Skript geschrieben wird:

#!/bin/sh
echo "$1" | sed 's/_test.rb$/_spec.rb/'

(Es kann eine perverse Art der Verwendung geben sh -c und eine Menge Anführungszeichen, um all das in einem Befehl zu tun, aber ich werde es nicht versuchen.)


31
2018-01-25 13:56



Um es auf eine Weise zu lösen, die dem ursprünglichen Problem am nächsten kommt, würde ich wahrscheinlich die Option "args per command line" von xargs verwenden:

find . -name *_test.rb | sed -e "p;s/test/spec/" | xargs -n2 mv

Er findet die Dateien im aktuellen Arbeitsverzeichnis rekursiv, gibt den ursprünglichen Dateinamen wieder (p) und dann ein modifizierter Name (s/test/spec/) und füttert alles zu mv in Paaren (xargs -n2). Beachten Sie, dass in diesem Fall der Pfad selbst keine Zeichenfolge enthalten sollte test.


104
2017-07-29 14:26



Vielleicht möchten Sie auch anders denken

for file in $(find . -name "*_test.rb")
do 
  echo mv $file `echo $file | sed s/_test.rb$/_spec.rb/`
done

22
2018-01-25 13:26



Ich finde diese kürzer

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

17
2017-10-16 10:15



Sie können es ohne sed tun, wenn Sie wollen:

for i in `find -name '*_test.rb'` ; do mv $i ${i%%_test.rb}_spec.rb ; done

${var%%suffix} Streifen suffix vom Wert von var.

oder, um es mit sed zu tun:

for i in `find -name '*_test.rb'` ; do mv $i `echo $i | sed 's/test/spec/'` ; done

9
2018-01-25 14:47



Sie erwähnen, dass Sie verwenden bash als deine Shell, in diesem Fall brauchst du nicht wirklich find und sed Um die Stapelumbenennung zu erreichen, nach der Sie suchen ...

Angenommen, Sie verwenden bash als deine Schale:

$ echo $SHELL
/bin/bash
$ _

... und vorausgesetzt, Sie haben das sogenannte globstar Shell-Option:

$ shopt -p globstar
shopt -s globstar
$ _

... und schließlich davon ausgehen, dass Sie das installiert haben rename Dienstprogramm (gefunden in der util-linux-ng Paket)

$ which rename
/usr/bin/rename
$ _

... dann können Sie die Stapelumbenennung in a erreichen Bash Einliner wie folgt:

$ rename _test _spec **/*_test.rb

(das globstar Die Shell-Option stellt sicher, dass bash alle passenden findet *_test.rb Dateien, egal wie tief sie in der Verzeichnishierarchie verschachtelt sind ... verwenden help shopt um herauszufinden, wie man die Option setzt)


9
2018-01-27 15:29



Der einfachste Weg:

find . -name "*_test.rb" | xargs rename s/_test/_spec/

Der schnellste Weg (vorausgesetzt, Sie haben 4 Prozessoren):

find . -name "*_test.rb" | xargs -P 4 rename s/_test/_spec/

Wenn Sie eine große Anzahl von Dateien verarbeiten müssen, ist es möglich, dass die Liste der Dateinamen, die an XARGS übergeben werden, dazu führen würde, dass die resultierende Befehlszeile die maximal zulässige Länge überschreitet.

Sie können das Limit Ihres Systems mit überprüfen getconf ARG_MAX 

Auf den meisten Linux-Systemen können Sie verwenden free -b oder cat /proc/meminfo um herauszufinden, mit wie viel Arbeitsspeicher Sie arbeiten müssen; Ansonsten, verwenden Sie top oder Ihre Systemaktivitätsüberwachungs-App.

Ein sicherer Weg (Vorausgesetzt, Sie haben 1000000 Bytes RAM zu arbeiten):

find . -name "*_test.rb" | xargs -s 1000000 rename s/_test/_spec/

5
2018-05-13 18:21



wenn Sie Ruby (1.9+) haben

ruby -e 'Dir["**/*._test.rb"].each{|x|test(?f,x) and File.rename(x,x.gsub(/_test/,"_spec") ) }'

1
2018-01-25 15:08



In Ramtams Antwort, die ich mag, funktioniert der Fundteil OK, aber der Rest funktioniert nicht, wenn der Pfad Leerzeichen enthält. Ich bin mit sed nicht sehr vertraut, aber ich konnte diese Antwort ändern zu:

find . -name "*_test.rb" | perl -pe 's/^((.*_)test.rb)$/"\1" "\2spec.rb"/' | xargs -n2 mv

Ich brauchte wirklich eine Änderung wie diese, weil in meinem Anwendungsfall der endgültige Befehl mehr wie aussieht

find . -name "olddir" | perl -pe 's/^((.*)olddir)$/"\1" "\2new directory"/' | xargs -n2 mv

1
2017-07-31 17:57



Ich habe nicht das Herz, es noch einmal zu tun, aber ich schrieb dies als Antwort auf Befehlszeile Finde Sed Exec. Dort wollte der Fragesteller wissen, wie man einen ganzen Baum verschiebt, möglicherweise ein oder zwei Verzeichnisse ausschließt, und alle Dateien und Verzeichnisse, die den String enthalten, umbenennen "ALT" stattdessen enthalten "NEU".

Außerdem Beschreiben der Wie mit mühevoller Ausführlichkeit unten kann diese Methode auch insofern einzigartig sein, als sie eingebautes Debugging beinhaltet. Es tut im Grunde nichts wie geschrieben, außer kompilieren und speichern Sie alle Befehle in einer Variablen, von denen es glaubt, dass es tun sollte, um die angeforderte Arbeit auszuführen.

Es auch explizit vermeidet Schleifen so viel wie möglich. neben dem sed rekursive Suche nach mehr als einer Übereinstimmung der Muster Soweit ich weiß, gibt es keine andere Rekursion.

Und zuletzt, das ist völlig null Begrenzt - es stolpert nicht auf irgendein Zeichen in irgendeinem Dateinamen außer dem null. Ich glaube nicht, dass du das haben solltest.

Das ist es übrigens JA WIRKLICH schnell. Aussehen:

% _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        

HINWEIS: Obenstehendes function wird wahrscheinlich erfordern GNU Versionen von sed und find richtig handhaben find printf und sed -z -e und :;recursive regex test;t Anrufe. Wenn diese Ihnen nicht zur Verfügung stehen, kann die Funktionalität wahrscheinlich mit ein paar kleineren Anpassungen dupliziert werden.

Dies sollte alles tun, was Sie wollten von Anfang bis Ende mit sehr wenig Aufwand. Ich tat fork mit sed, aber ich habe auch etwas geübt sed rekursive Verzweigungstechniken, deshalb bin ich hier. Es ist so, als würde man in einer Friseurschule einen Rabatt-Haarschnitt bekommen, schätze ich. Hier ist der Workflow:

  • rm -rf ${UNNECESSARY}
    • Ich verzichtete absichtlich auf jeden Funktionsaufruf, der Daten jeglicher Art löschen oder zerstören könnte. Sie erwähnen das ./app könnte unerwünscht sein. Löschen Sie es oder verschieben Sie es vorher woanders, oder alternativ könnten Sie in a \( -path PATTERN -exec rm -rf \{\} \) Routine zu find um es programmatisch zu machen, aber das gehört alles dir.
  • _mvnfind "${@}"
    • Deklarieren Sie die Argumente und rufen Sie die Worker-Funktion auf. ${sh_io} ist besonders wichtig, da es die Rückkehr von der Funktion speichert. ${sed_sep} kommt in einer nahen Sekunde; Dies ist eine beliebige Zeichenfolge, die zum Referenzieren verwendet wird sedRekursion in der Funktion. Ob ${sed_sep} ist auf einen Wert eingestellt, der möglicherweise in irgendeinem Ihrer Pfad- oder Dateinamen gefunden werden könnte, auf ... naja, lassen Sie es einfach nicht.
  • mv -n $1 $2
    • Der ganze Baum wird von Anfang an bewegt. Es wird eine Menge Kopfschmerzen sparen; Das können Sie mir glauben. Der Rest von dem, was Sie tun wollen - das Umbenennen -, ist einfach eine Angelegenheit von Metadaten des Dateisystems. Wenn Sie zum Beispiel diese von einem Laufwerk auf ein anderes oder über Dateisystemgrenzen hinweg verschieben, ist es besser, wenn Sie dies gleichzeitig mit einem einzigen Befehl tun. Es ist auch sicherer. Beachten Sie das -noclobber Option festgelegt für mv; wie geschrieben, wird diese Funktion nicht gesetzt ${SRC_DIR} wo ein ${TGT_DIR} ist bereits vorhanden.
  • read -R SED <<HEREDOC
    • Ich habe alle Befehle von sed hier gefunden, um bei fliehenden Problemen zu sparen und sie in eine Variable einzulesen, um sed darunter zu füttern. Erklärung unten.
  • find . -name ${OLD} -printf
    • Wir beginnen mit der find verarbeiten. Mit find wir suchen nur nach etwas, das umbenannt werden muss, weil wir bereits alle von Ort zu Ort gemacht haben mv Operationen mit dem ersten Befehl der Funktion. Anstatt direkt etwas zu unternehmen find, wie ein exec Rufen wir zum Beispiel an, verwenden wir stattdessen, um die Befehlszeile dynamisch mit zu erweitern -printf.
  • %dir-depth :tab: 'mv '%path-to-${SRC}' '${sed_sep}'%path-again :null delimiter:'
    • Nach find lokalisiert die Dateien, die wir benötigen, erstellt und druckt sie direkt (die meisten) des Befehls müssen wir Ihre Umbenennung verarbeiten. Das %dir-depth An den Anfang jeder Zeile angeheftet, wird sichergestellt, dass wir nicht versuchen, eine Datei oder ein Verzeichnis in der Struktur mit einem übergeordneten Objekt umzubenennen, das noch nicht umbenannt wurde. find verwendet alle Arten von Optimierungstechniken, um Ihre Dateisystemstruktur zu durchlaufen, und es ist nicht sicher, dass die Daten, die wir benötigen, in einer "Safe-for-Operations" -Reihenfolge zurückgegeben werden. Deshalb sind wir nächste ...
  • sort -general-numerical -zero-delimited
    • Wir sortieren alle findDie Ausgabe basiert auf %directory-depth damit die Pfade, die am nächsten zu $ ​​{SRC} stehen, zuerst bearbeitet werden. Dies vermeidet mögliche Fehler mvSie können Dateien in nicht vorhandene Speicherorte einfügen und minimieren die Notwendigkeit für rekursive Schleifen. (In der Tat könnte es schwierig sein, eine Schleife zu finden)
  • sed -ex :rcrs;srch|(save${sep}*til)${OLD}|\saved${SUBSTNEW}|;til ${OLD=0}
    • Ich denke, das ist die einzige Schleife im ganzen Skript, und es dreht sich nur über die zweite %Path für jeden String gedruckt, falls es mehr als einen $ {OLD} -Wert enthält, der ersetzt werden muss. Alle anderen Lösungen, die ich mir vorgestellt habe, haben eine zweite Rolle gespielt sed Prozess, und während eine kurze Schleife nicht wünschenswert sein kann, schlägt es sicherlich das Laichen und Forken eines ganzen Prozesses.
    • Also im Grunde was sed sucht hier nach $ {sed_sep}, speichert es und alle gefundenen Zeichen, bis es $ {OLD} findet und es durch $ {NEW} ersetzt. Dann kehrt es zu $ ​​{sed_sep} zurück und sucht erneut nach $ {OLD}, falls es mehr als einmal in der Zeichenkette vorkommt. Wenn es nicht gefunden wird, druckt es die modifizierte Zeichenkette auf stdout (was es dann wieder als nächstes fängt) und beendet die Schleife.
    • Dadurch wird vermieden, dass die gesamte Zeichenfolge analysiert werden muss, und sichergestellt, dass die erste Hälfte von mv Die Befehlszeichenfolge, die natürlich $ {OLD} enthalten muss, enthält sie, und die zweite Hälfte wird so oft geändert, wie es nötig ist, um den Namen $ {OLD} zu löschen mvZielpfad.
  • sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
    • Die Zwei -exec Anrufe hier passieren ohne eine Sekunde fork. In der ersten, wie wir gesehen haben, modifizieren wir die mv Befehl wie von findist es -printf function-Befehl als notwendig, um alle Referenzen von $ {OLD} zu $ ​​{NEW} richtig zu ändern, aber um dies zu tun, mussten wir einige beliebige Referenzpunkte verwenden, die nicht in der endgültigen Ausgabe enthalten sein sollten. Also einmal sed beendet alles, was es tun muss, weisen wir es an, seine Referenzpunkte aus dem Haltepuffer auszulöschen, bevor Sie es weiterleiten.

Und jetzt sind wir wieder da

read wird einen Befehl erhalten, der folgendermaßen aussieht:

% mv /path2/$SRC/$OLD_DIR/$OLD_FILE /same/path_w/$NEW_DIR/$NEW_FILE \000

Es wird read es in ${msg} wie ${sh_io} was außerhalb der Funktion beliebig überprüft werden kann.

Cool.

-Mike


1
2017-12-11 09:11



Ich konnte Dateinamen mit Leerzeichen umgehen, indem ich folge die Beispiele vorgeschlagen von onitake.

Dies nicht break, wenn der Pfad Leerzeichen oder die Zeichenfolge enthält test:

find . -name "*_test.rb" -print0 | while read -d $'\0' file
do
    echo mv "$file" "$(echo $file | sed s/test/spec/)"
done

1
2017-12-03 23:04