Ich habe eine Reihe von XML-Dateien in einem Verzeichnisbaum, die ich gerne in entsprechende Ordner mit demselben Namen innerhalb desselben Verzeichnisbaums verschieben möchte.
Hier ist eine Beispielstruktur (in der Shell):
touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml"
mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"
Mein Ansatz hier ist also:
find . -name "*.xml" -exec sh -c '
DST=$(
find . -type d -name "$(basename "{}" .xml)" -print -quit
)
[ -d "$DST" ] && mv -v "{}" "$DST/"' ';'
Das Ergebnis ist folgendes:
‘./( bar ).xml’ -> ‘./bar/( bar )/( bar ).xml’
mv: ‘./bar/( bar )/( bar ).xml’ and ‘./bar/( bar )/( bar ).xml’ are the same file
‘./bar.xml’ -> ‘./bar/bar.xml’
‘./foo.xml’ -> ‘./foo/foo.xml’
Aber die Datei mit den eckigen Klammern ( [ foo ].xml
) wurde nicht verschoben, als ob sie ignoriert worden wäre.
Ich habe es überprüft und basename
(eg basename "[ foo ].xml" ".xml"
) konvertiert die Datei korrekt, find
hat jedoch Probleme mit Klammern. Zum Beispiel:
find . -name '[ foo ].xml'
findet die Datei nicht richtig. Wenn man die Klammern ( '\[ foo \].xml'
) jedoch entfernt, funktioniert es einwandfrei, aber es löst das Problem nicht, weil es Teil des Skripts ist und ich nicht weiß, welche Dateien diese speziellen (Shell-?) Zeichen haben. Getestet mit BSD und GNU find
.
Gibt es eine universelle Möglichkeit, die Dateinamen zu maskieren, wenn ich find
den -name
Parameter with verwende, sodass ich meinen Befehl korrigieren kann, um Dateien mit den Metazeichen zu unterstützen?
Antwort1
zsh
Mit Globs hier ist es viel einfacher :
for f (**/*.xml(.)) (mv -v -- $f **/$f:r:t(/[1]))
Oder wenn Sie versteckte XML-Dateien einschließen und in versteckten Verzeichnissen nachsehen möchten, wie find
es der Fall wäre:
for f (**/*.xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))
.xml
Beachten Sie jedoch , dass Dateien mit den Namen ..xml
oder ...xml
zum Problem werden könnten. Sie sollten sie daher möglicherweise ausschließen:
setopt extendedglob
for f (**/(^(|.|..)).xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))
Um nicht den gesamten Verzeichnisbaum nach jeder Datei durchsuchen zu müssen, gibt es mit GNU-Tools einen weiteren Ansatz: Einmal scannen und nach allen Verzeichnissen und xml
Dateien suchen, aufzeichnen, wo sie sich befinden und am Ende das Verschieben durchführen:
(export LC_ALL=C
find . -mindepth 1 -name '*.xml' ! -name .xml ! \
-name ..xml ! -name ...xml -type f -printf 'F/%P\0' -o \
-type d -printf 'D/%P\0' | awk -v RS='\0' -F / '
{
if ($1 == "F") {
root = $NF
sub(/\.xml$/, "", root)
F[root] = substr($0, 3)
} else D[$NF] = substr($0, 3)
}
END {
for (f in F)
if (f in D)
printf "%s\0%s\0", F[f], D[f]
}' | xargs -r0n2 mv -v --
)
Ihr Ansatz weist eine Reihe von Problemen auf, wenn Sie beliebige Dateinamen zulassen möchten:
- Einbettung
{}
in den Shell-Code iststets$(rm -rf "$HOME").xml
falsch. Was ist, wenn es beispielsweise eine Datei gibt ? Der richtige Weg besteht darin, diese{}
als Argument an das Inline-Shell-Skript (-exec sh -c 'use as "$1"...' sh {} \;
) zu übergeben. - Mit GNU
find
(hier impliziert, da Sie verwenden-quit
)*.xml
würde nur auf Dateien zutreffen, die aus einer Folge gültiger Zeichen gefolgt von bestehen.xml
. Dadurch werden Dateinamen ausgeschlossen, die im aktuellen Gebietsschema ungültige Zeichen enthalten (beispielsweise Dateinamen im falschen Zeichensatz). Die Lösung hierfür besteht darin, das Gebietsschema so zu ändern, dassC
jedes Byte ein gültiges Zeichen ist (das bedeutet jedoch, dass Fehlermeldungen auf Englisch angezeigt werden). - Wenn eine dieser
xml
Dateien vom Typ „Verzeichnis“ oder „Symlink“ ist, kann das Probleme verursachen (das Scannen von Verzeichnissen beeinträchtigen oder Symlinks beim Verschieben beschädigen). Sie sollten möglicherweise ein hinzufügen,-type f
um nur normale Dateien zu verschieben. - Befehlsersetzung (
$(...)
) Streifenallenachfolgende Zeilenumbruchzeichen. Das würde Probleme mit einer Datei verursachen, diefoo.xml
beispielsweise aufgerufen wird. Das Umgehen ist möglich, aber mühsam:base=$(basename "$1" .xml; echo .); base=${base%??}
. Sie können zumindestbasename
durch die${var#pattern}
Operatoren ersetzen. Und vermeiden Sie nach Möglichkeit die Befehlsersetzung. - Ihr Problem mit Dateinamen, die Platzhalterzeichen (, und Backslashs enthalten ;
?
sie sind nicht speziell für die Shell, sondern für die von ihr durchgeführte Musterübereinstimmung ( ) , die der Musterübereinstimmung der Shell sehr ähnlich ist). Sie müssten sie mit einem Backslash maskieren.[
*
fnmatch()
find
- das oben erwähnte Problem mit
.xml
,..xml
, ....xml
Wenn wir also alle oben genannten Punkte berücksichtigen, erhalten wir etwa Folgendes:
LC_ALL=C find . -type f -name '*.xml' ! -name .xml ! -name ..xml \
! -name ...xml -exec sh -c '
for file do
base=${file##*/}
base=${base%.xml}
escaped_base=$(printf "%s\n" "$base" |
sed "s/[[*?\\\\]/\\\\&/g"; echo .)
escaped_base=${escaped_base%??}
find . -name "$escaped_base" -type d -exec mv -v "$file" {\} \; -quit
done' sh {} +
Puh...
Aber das ist noch nicht alles. Mit -exec ... {} +
führen wir so wenige sh
wie möglich aus. Wenn wir Glück haben, führen wir nur eine aus, wenn nicht, sh
haben wir nach dem ersten Aufruf eine Reihe von xml
Dateien verschoben und find
suchen dann weiter nach weiteren. Möglicherweise finden wir die Dateien, die wir in der ersten Runde verschoben haben, wieder (und versuchen höchstwahrscheinlich, sie dorthin zu verschieben, wo sie sind).
Ansonsten ist es im Grunde der gleiche Ansatz wie bei zsh. Ein paar weitere bemerkenswerte Unterschiede:
- Bei der
zsh
einen wird die Dateiliste sortiert (nach Verzeichnisnamen und Dateinamen), sodass das Zielverzeichnis mehr oder weniger konsistent und vorhersehbar ist. Beifind
basiert es auf der Rohreihenfolge der Dateien in den Verzeichnissen. - Bei
zsh
wird eine Fehlermeldung angezeigt, wenn kein passendes Verzeichnis zum Verschieben der Datei gefunden wird. Bei derfind
obigen Vorgehensweise ist dies nicht der Fall. - Bei
find
erhalten Sie Fehlermeldungen, wenn manche Verzeichnisse nicht durchsucht werden können, bei diesem jedoch nichtzsh
.
Eine letzte Warnung. Wenn der Grund, warum Sie Dateien mit zweifelhaften Namen erhalten, darin liegt, dass der Verzeichnisbaum von einem Angreifer beschreibbar ist, dann seien Sie sich bewusst, dass keine der oben genannten Lösungen sicher ist, wenn der Angreifer Dateien unter dem Kommando dieses Angreifers umbenennen kann.
Wenn Sie beispielsweise LXDE verwenden, könnte der Angreifer einen bösartigen Befehl erstellen foo/lxde-rc.xml
, einen Ordner erstellen lxde-rc
, erkennen, wann Sie Ihren Befehl ausführen, und diesen während des Race-Fensters (das auf viele Arten beliebig groß gemacht werden kann) zwischen dem Erkennen und der Ausführung des Befehls lxde-rc
durch einen symbolischen Link zu Ihrem ersetzen ( der auch in diesen symbolischen Link geändert werden könnte, sodass Sie Ihren Befehl woanders hin verschieben).~/.config/openbox/
find
lxde-rc
mv
rename("foo/lxde-rc.xml", "lxde-rc/lxde-rc.xml")
foo
lxde-rc.xml
Dies lässt sich mit Standard- oder sogar GNU-Dienstprogrammen wahrscheinlich nicht umgehen. Sie müssten es in einer richtigen Programmiersprache schreiben, einige sichere Verzeichnisdurchläufe durchführen und renameat()
Systemaufrufe verwenden.
Alle oben genannten Lösungen schlagen auch fehl, wenn der Verzeichnisbaum so tief ist, dass die Beschränkung der Pfadlänge für den rename()
Systemaufruf von mv
erreicht wird (was rename()
zu einem Fehler mit führt ENAMETOOLONG
). Eine Lösung mit renameat()
würde das Problem ebenfalls umgehen.
Antwort2
Wenn Sie ein Inline-Skript mit verwenden find ... -exec sh -c ...
, sollten Sie find
das Ergebnis über den Positionsparameter an die Shell übergeben, sodass Sie es nicht {}
überall in Ihrem Inline-Skript verwenden müssen.
Wenn Sie bash
oder haben zsh
, können Sie basename
die Ausgabe durch Folgendes weitergeben printf '%q'
:
find . -name "*.xml" -exec bash -c '
for f do
BASENAME="$(printf "%q" "$(basename -- "$f" .xml)")"
DST=$(find . -type d -name "$BASENAME" -print -quit)
[ -d "$DST" ] && mv -v -- "$f" "$DST/"
done
' bash {} +
Mit bash
können Sie verwenden printf -v BASENAME
, und dieser Ansatz funktioniert nicht richtig, wenn der Dateiname Steuerzeichen oder Nicht-ASCII-Zeichen enthält.
Wenn es ordnungsgemäß funktionieren soll, müssen Sie eine Shell-Funktion schreiben, um nur [
, *
und ?
den Backslash zu escapen.
Antwort3
Die guten Nachrichten:
find . -name '[ foo ].xml'
wird von der Shell nicht interpretiert, sondern so an das Find-Programm weitergegeben. Find interpretiert das Argument jedoch -name
als glob
Muster und dies muss berücksichtigt werden.
Wenn Sie anrufen find -exec \;
oder besser sagen möchten find -exec +
, ist keine Shell erforderlich.
Wenn Sie die Ausgabe über die Shell verarbeiten möchten find
, empfehle ich, das Dateinamen-Globbing in der Shell einfach durch einen Aufruf set -f
vor dem betreffenden Code zu deaktivieren und durch einen späteren Aufruf wieder einzuschalten set +f
.
Antwort4
Das Folgende ist eine relativ unkomplizierte, POSIX-kompatible Pipeline. Sie durchsucht die Hierarchie zweimal, zuerst nach Verzeichnissen und dann nach regulären *.xml-Dateien. Eine leere Zeile zwischen den Scans signalisiert AWK den Übergang.
Die AWK-Komponente ordnet Basisnamen Zielverzeichnissen zu (wenn es mehrere Verzeichnisse mit demselben Basisnamen gibt, wird nur der erste Durchlauf gespeichert). Für jede *.xml-Datei wird eine tabulatorgetrennte Zeile mit zwei Feldern gedruckt: 1) der Pfad der Datei und 2) das entsprechende Zielverzeichnis.
{
find . -type d
echo
find . -type f -name \*.xml
} |
awk -F/ '
!NF { ++i; next }
!i && !($NF".xml" in d) { d[$NF".xml"] = $0 }
i { print $0 "\t" d[$NF] }
' |
while IFS=' ' read -r f d; do
mv -- "$f" "$d"
done
Der IFS unmittelbar vor dem Lesen zugewiesene Wert ist ein Tabulatorzeichen und kein Leerzeichen.
Hier ist eine Abschrift unter Verwendung des Touch/Mkdir-Gerüsts der ursprünglichen Frage:
$ touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml"
$ mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"
$ find .
.
./foo
./foo/[ foo ]
./bar.xml
./foo.xml
./bar
./bar/( bar )
./[ foo ].xml
./( bar ).xml
$ ../mv-xml.sh
$ find .
.
./foo
./foo/[ foo ]
./foo/[ foo ]/[ foo ].xml
./foo/foo.xml
./bar
./bar/( bar )
./bar/( bar )/( bar ).xml
./bar/bar.xml