Wie kann man Shell-Metazeichen automatisch mit dem Befehl „find“ escapen?

Wie kann man Shell-Metazeichen automatisch mit dem Befehl „find“ escapen?

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, findhat 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 findden -nameParameter with verwende, sodass ich meinen Befehl korrigieren kann, um Dateien mit den Metazeichen zu unterstützen?

Antwort1

zshMit 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 findes der Fall wäre:

for f (**/*.xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))

.xmlBeachten Sie jedoch , dass Dateien mit den Namen ..xmloder ...xmlzum 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 xmlDateien 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").xmlfalsch. 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) *.xmlwü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, dass Cjedes Byte ein gültiges Zeichen ist (das bedeutet jedoch, dass Fehlermeldungen auf Englisch angezeigt werden).
  • Wenn eine dieser xmlDateien 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 fum nur normale Dateien zu verschieben.
  • Befehlsersetzung ( $(...)) Streifenallenachfolgende Zeilenumbruchzeichen. Das würde Probleme mit einer Datei verursachen, die foo␤.xmlbeispielsweise aufgerufen wird. Das Umgehen ist möglich, aber mühsam: base=$(basename "$1" .xml; echo .); base=${base%??}. Sie können zumindest basenamedurch 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 shwie möglich aus. Wenn wir Glück haben, führen wir nur eine aus, wenn nicht, shhaben wir nach dem ersten Aufruf eine Reihe von xmlDateien verschoben und findsuchen 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 zsheinen wird die Dateiliste sortiert (nach Verzeichnisnamen und Dateinamen), sodass das Zielverzeichnis mehr oder weniger konsistent und vorhersehbar ist. Bei findbasiert es auf der Rohreihenfolge der Dateien in den Verzeichnissen.
  • Bei zshwird eine Fehlermeldung angezeigt, wenn kein passendes Verzeichnis zum Verschieben der Datei gefunden wird. Bei der findobigen Vorgehensweise ist dies nicht der Fall.
  • Bei finderhalten Sie Fehlermeldungen, wenn manche Verzeichnisse nicht durchsucht werden können, bei diesem jedoch nicht zsh.

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-rcdurch 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/findlxde-rcmvrename("foo/lxde-rc.xml", "lxde-rc/lxde-rc.xml")foolxde-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 mverreicht 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 finddas Ergebnis über den Positionsparameter an die Shell übergeben, sodass Sie es nicht {}überall in Ihrem Inline-Skript verwenden müssen.

Wenn Sie bashoder haben zsh, können Sie basenamedie 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 bashkö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 -nameals globMuster 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 -fvor 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

verwandte Informationen