
Angenommen, ich habe zwei Textdateien src.txt
und dest.txt
, wobei src.txt
in eine Liste von Dateinamen (einige davon mit Leerzeichen) /src/dir/
und dest.txt
in zufälliger Reihenfolge eine Liste der vollständigen Dateipfade (wiederum mit Leerzeichen) enthalten ist, zu denen sie gehören. Beispiel:
quelle.txt:
file 1.jpg
file_2.html
file 3.jpg
dest.txt:
/dest/dir 1/file 3.jpg
/dest/file4.txt
/dest/file 5.txt
/dest/dir 2/file 1.jpg
/dest/file_2.html
Wie kann ich diesen Batch-Verschiebevorgang von der Shell aus durchführen? Ich habe mit einer while read
Schleife über die Quelldatei gearbeitet und bin ziemlich sicher, dass ich den mv
Befehl verwenden muss, aber ich bin mir nicht sicher, ob grep
er sed
hier benötigt wird. Ich stoße immer wieder auf cannot stat...
Fehler bei der Auflösung von Leerzeichen.
Antwort1
Mit zsh
:
src=(${(f)"$(<src.txt)"})
for f (${(f)"$(<dest.txt)"})
(($src[(Ie)$f:t])) && mv /src/dir/$f:t $f
Dadurch wird jede Datei in einem Array gelesen und dann für jedes Element in der"Ziel"Array, wenn der Basisname ( :t
ist ein zsh
Modifikator, der alle führenden Pfadnamenkomponenten entfernt) auch im"Quelle"Array, dann verschiebt es die Datei. Um einen Probelauf durchzuführen, ersetzen Sie mv
durch printf '"%s" -> "%s"\n'
.
Sie können nun auch Folgendes ausführen (immer noch in zsh
):
for f (${(f)"$(grep -Ff src.txt dest.txt)"})
mv /src/dir/$f:t $f
was gut funktioniert, solange keiner der Dateinamen in mit src.txt
einem der Verzeichnisnamen (oder Teilen dieser Namen) in der Liste der Pfade in übereinstimmt dest.txt
(z. B. würde ein Dateiname data1
in src.txt
und ein Pfad wie /path/data1_dir/some_file
in dest.txt
ein falsches Positiv ergeben). Um das zu vermeiden, können Sie die Dateinamen als grep
Muster (d. h. mit regulären Ausdrücken wie /filename$
) statt als F
feste Zeichenfolgen übergeben, um nur die letzte Komponente der Pfade in abzugleichen dest.txt
. Dies erfordert allerdings das Maskieren aller Sonderzeichen (falls vorhanden) in Dateinamen in src.txt
, z. B. dieses Mal mit bash
( 4
):
readarray -t files < <(sed 's|[[\.*^$/]|\\&|g;s|.*|/&$|' src.txt | grep -f- dest.txt)
for f in "${files[@]}"; do mv /src/dir/"${f##*/}" "$f"; done
Antwort2
Wenn ein Zeilenumbruch ein akzeptables Trennzeichen ist, sollte das Folgende in einer POSIX-Shell ziemlich robust sein:
IFS='
';set -f
for f in $(cat <"$destfile")
do [ -e "./${f##*/}" ] ||
[ -h "./${f##*/}" ] &&
mv "./${f##*/}" "$f"
done
Ich kann mir zwei mögliche Probleme mit dieser Lösung vorstellen:
Die Eingabedatei ist einfach zu groß, um sie auf diese Weise auf einmal aufzuteilen.
- Auf meinem System ist dies erst dann wirklich eine ernsthafte Überlegung wert, wenn die Eingabe viele Zehntausend Zeilen umfasst.
Ein Dateiname in
$destfile
könnte im aktuellen Verzeichnis vorhanden sein und sollte dennochnichttrotzdem verschoben werden.- Da bei dieser Lösung auf den Vergleich der beiden Eingabedateien vollständig verzichtet wird und nur die
$destfile
Existenz der letzten Pfadnamenkomponente im aktuellen Verzeichnis geprüft wird, sollte eine unbeabsichtigte Übereinstimmung der Dateinamen nicht berücksichtigt werden.
- Da bei dieser Lösung auf den Vergleich der beiden Eingabedateien vollständig verzichtet wird und nur die
Wenn nur das erste Problem behoben werden muss:
sed -ne"s|'|'"'\\&&|g' <"$destfile" \
-e "s|.*/\([^/].*\)|_mv './\1' '&'|p" |
sh -c '_mv(){ [ -e "$1" ]||[ -h "$1" ]&& mv "$@";};. /dev/fd/0'
Wenn dies bei Ihnen der sh
Fall ist, dash
können Sie das . /dev/fd/0
am Ende weglassen und Folgendes verwenden:
sed ... | sh -cs '_mv(){ ...;}'
...weil dash
seltsamerweise sowohl die Befehlszeilen- als auch die Standardeingabeoptionen gleichzeitig und ohne Beanstandungen verarbeitet werden. Das wäre nicht sehr portabel, aber . /dev/fd/0
– obwohl es ziemlich portabel ist – ist es auch nicht streng standardkonform.
Wenn das zweite Problem Anlass zur Sorge gibt:
export LC_ALL=C
sed -ne'\|/$|!s|.*/\(.*\)|\1/&|p' <"$destfile" |
sort -t/ -k1,1 - ./"$srcfile" | cut -d/ -f2- |
sed -e "\|/|!N;\|\n.*/|!d" \
-e "s|'|'"'\\&&|g' \
-e "s|\n|' '|;s|.*|mv './&'|" | sh
... das sollte damit sehr gut zurechtkommen, solange alle Dateinamen in ./"$srcfile"
richtig und identisch am Ende eines Pfades in berücksichtigt werden "$destfile"
. sort
wird immer den kürzeren von zwei ansonsten identischen Vergleichen nach oben stellen. Wenn also nur das erste Feld zählt und der Dateiname vor den Anfang jedes Pfadnamens von gestellt wird, wird "$destfile"
eine Zusammenführung sort
beider Dateien Sequenzen wie die folgenden ausgeben:
$srcfile: no /
$destfile: match
$destfile: unique
$destfile: unique
...
$srcfile: no /
$destfile: match
$destfile: unique
...und Sie müssen sich daher nur mit den Zeilenpaaren befassen, die mit einer nicht übereinstimmenden Zeile beginnen /
.
Antwort3
while read i; do echo cp \""$i"\" \"$(grep "/$i$" dst.txt)\"; done < src.txt
Dadurch wird ausgedruckt, was getan worden wäre. Entfernen Sie einfach das, echo
um die Dateien tatsächlich zu kopieren.
Antwort4
Ein einzeiliges Skript generiert ein Skript, das ein Skript generiert.
In diesem Beispiel verwenden wir einen ersten Aufruf von sed
on, src.txt
um ein zweites Skript zu generieren sed
, das auf ausgeführt wird dest.txt
, um ein Shell-Skript zum Kopieren der Dateien zu generieren.
Hier ist der Einzeiler:
$ sed -n "$(sed 's,\(..*\),/\\/\1$/ { s/^/cp "\1" "/; s/$/";/; p; },' src.txt)" dest.txt #| sh -x
und die Ausgabe:
cp "file 3.jpg" "/dest/dir 1/file 3.jpg";
cp "file 1.jpg" "/dest/dir 2/file 1.jpg";
cp "file_2.html" "/dest/file_2.html";
Beachten Sie den Kommentar #| sh
am Ende des Befehls. Auf diese Weise können Sie den Befehl ausprobieren und sehen, was er bewirkt. Wenn alles gut ist, entfernen Sie das Kommentarzeichen aus der Pipe sh
und kopieren Sie die Dateien wirklich.
Der innere sed-Befehl erstellt ein sed-Skript aus src.txt. Die erste Zeile des generierten Skripts sieht folgendermaßen aus:
/\/file 1.jpg$/ { s/^/cp file 1.jpg /; p; }
So funktioniert es:
Eingang:
$ cat src.txt
file 1.jpg
file_2.html
file 3.jpg
$ cat dest.txt
/dest/dir 1/file 3.jpg
/dest/file4.txt
/dest/file 5.txt
/dest/dir 2/file 1.jpg
/dest/file_2.html
Erster sed
Aufruf. Dies zeigt das generierte Skript, das beim zweiten Aufruf von interpretiert wird sed
:
$ sed 's,\(..*\),/\\/\1$/ { s/^/cp "\1" "/; s/$/";/; p; },' src.txt
/\/file 1.jpg$/ { s/^/cp "file 1.jpg" "/; s/$/";/; p; }
/\/file_2.html$/ { s/^/cp "file_2.html" "/; s/$/";/; p; }
/\/file 3.jpg$/ { s/^/cp "file 3.jpg" "/; s/$/";/; p; }
Verwenden Sie die Shell-Befehlsersetzung, um die Ausgabe des ersten sed
Befehls als Skript in der Befehlszeile zu verwenden, das an den zweiten Aufruf von übergeben wird sed
:
$ sed -n "$(sed 's,\(..*\),/\\/\1$/ { s/^/cp "\1" "/; s/$/";/; p; },' src.txt)" dest.txt
cp "file 3.jpg" "/dest/dir 1/file 3.jpg";
cp "file 1.jpg" "/dest/dir 2/file 1.jpg";
cp "file_2.html" "/dest/file_2.html";
Leiten Sie nun die Ausgabe von sed mit der Option xtrace ( sh -x
) an die Shell weiter. Ich habe keine der Dateien, daher die Fehler:
$ sed -n "$(sed 's,\(..*\),/\\/\1$/ { s/^/cp "\1" "/; s/$/";/; p; },' src.txt)" dest.txt | sh -x
+ cp file 3.jpg /dest/dir 1/file 3.jpg
cp: cannot stat ‘file 3.jpg’: No such file or directory
+ cp file 1.jpg /dest/dir 2/file 1.jpg
cp: cannot stat ‘file 1.jpg’: No such file or directory
+ cp file_2.html /dest/file_2.html
cp: cannot stat ‘file_2.html’: No such file or directory