Der effizienteste Weg, eine Zeile in einer Datei zu ändern

Der effizienteste Weg, eine Zeile in einer Datei zu ändern

Ich möchte die erste Zeile von Hunderten von Dateien rekursiv und auf möglichst effiziente Weise ändern. Ein Beispiel für das, was ich tun möchte, ist die Änderung #!/bin/bashin #!/bin/sh, also habe ich mir diesen Befehl ausgedacht:

find ./* -type f -exec sed -i '1s/^#!\/bin\/bash/#!\/bin\/sh/' {} \;

Aber nach meinem Verständnis muss sed bei dieser Vorgehensweise die gesamte Datei lesen und das Original ersetzen. Gibt es eine effizientere Möglichkeit, dies zu tun?

Antwort1

Ja, sed -iliest und schreibt die Datei vollständig neu, und da sich die Zeilenlänge ändert, muss dies geschehen, da die Positionen aller anderen Zeilen verschoben werden.

...aber in diesem Fall muss die Zeilenlänge eigentlich nicht geändert werden. Wir können die Hashbang-Zeile #!/bin/sh␣␣stattdessen durch ersetzen, mit zwei abschließenden Leerzeichen. Das Betriebssystem entfernt diese beim Parsen der Hashbang-Zeile. (Alternativ können Sie zwei Zeilenumbrüche oder einen Zeilenumbruch + Rautezeichen verwenden, beides erzeugt zusätzliche Zeilen, die die Shell letztendlich ignoriert.)

Alles, was wir tun müssen, ist, die Datei von Anfang an zum Schreiben zu öffnen, ohne sie abzuschneiden. Die üblichen Umleitungen >können >>das nicht, aber in Bash <>scheint die Lese-/Schreibumleitung zu funktionieren:

echo '#!/bin/sh  ' 1<> foo.sh

oder verwenden Sie dd(dies sollten Standard-POSIX-Optionen sein):

echo '#!/bin/sh  ' | dd of=foo.sh conv=notrunc

Beachten Sie, dass beide streng genommen auch den Zeilenumbruch am Zeilenende neu schreiben, aber das spielt keine Rolle.

Natürlich überschreibt das Obige den Anfang der angegebenen Datei bedingungslos. Das Hinzufügen einer Prüfung, ob die Originaldatei den richtigen Hashbang hat, bleibt als Übung übrig ... Unabhängig davon würde ich dies in der Produktion wahrscheinlich nicht tun, und offensichtlich wird dies nicht funktionieren, wenn Sie die Zeile in eine ändern müssenlängereins.

Antwort2

Eine Optimierung wäre, {} +anstelle von zu verwenden {} \;.

find . -type f -exec sed -i '1s|^#!/bin/bash|#!/bin/sh|' {} +

Anstatt für jede gefundene Datei einen Sed-Prozess aufzurufen, stellen Sie die Dateien als Argumente einem einzelnen Sed-Prozess zur Verfügung.

POSIX-Spezifikation von „find on“{} +(meine Hervorhebung):

Wenn der primäre Ausdruck durch ein <Pluszeichen> getrennt ist, wird der primäre Ausdruck immer als „true“ ausgewertet und die Pfadnamen, für die der primäre Ausdruck ausgewertet wird, werden zu Mengen zusammengefasst.Das Dienstprogramm utility_name muss einmal für jeden Satz aggregierter Pfadnamen aufgerufen werden.

Antwort3

Ja, würde ich:

#! /bin/zsh -
LC_ALL=C # work with bytes instead of characters.
shebang_to_replace=$'#!/bin/bash\n'
       new_shebang=$'#!/bin/sh -\n'

length=$#shebang_to_replace

ret=0
for file in **/*(N.L+$((length - 1)));do
  if
    read -u0 -k $length shebang < $file &&
      [[ $shebang = $shebang_to_replace ]]
  then
    print -rn -- $new_shebang 1<> $file || ret=$?
  fi
done
exit $ret

Wie@ilkkachus Ansatzwird die Datei an Ort und Stelle mit einem String überschrieben, der genau dieselbe Größe hat. Die Unterschiede sind:

  • wir ignorieren versteckte Dateien und Dateien in versteckten Verzeichnissen (denken Sie .gitzum Beispiel an eins), da es unwahrscheinlich ist, dass Sie diese berücksichtigen möchten (Sie haben find ./*which verwendet, das die versteckten Dateien und Verzeichnisse des aktuellen Verzeichnisses übersprungen hätte, aber nicht die der Unterverzeichnisse). Fügen Sie den DGlob-Qualifizierer hinzu, wenn Sie sie möchten.
  • wir machen uns nicht die Mühe, in Dateien nachzuschauen, die nicht groß genug sind, um den zu ersetzenden Original-Shebang aufzunehmen (wir verwenden .als Äquivalent zu -type f, da wir die Inode-Informationen bereits aus der Datei abrufen, können wir die Größe auch gleich dort überprüfen).
  • Wir prüfen tatsächlich, ob die Datei mit dem richtigen zu ersetzenden Shebang beginnt, und lesen so wenig Bytes wie nötig (hier muss dies zshso sein, da andere Shells nicht mit beliebigen Bytewerten umgehen können).
  • Wir verwenden #!/bin/sh -als Ersatz das richtige Shebang für /bin/shSkripte ( #!/bin/bash -das wäre /bin/bashübrigens das richtige Shebang für Skripte). SieheWarum das "-" im "#! /bin/sh -"-Shebang?für Details.

Im Beendigungsstatus werden Fehler beim Überschreiben von Dateien gemeldet, nicht jedoch Fehler beim Durchsuchen des Verzeichnisbaums oder beim Lesen der Dateien, obwohl diese hinzugefügt werden könnten.

Auf jeden Fall ersetzt es nur die Sachen, diegenau #!/bin/bash, keine anderen Shebangs, die bashals Interpreter verwendet werden, wie #! /bin/bash, #! /bin/bash -Oextglob, #! /usr/bin/env bash, #! /bin/bash -efu. Für diese müssen Sie entscheiden, was zu tun ist. -efusind shOptionen, -Oextglobhaben aber shbeispielsweise kein Äquivalent.

Sie können es erweitern, um die einfachsten Fälle zu unterstützen, wie:

#! /bin/zsh -
LC_ALL=C # work with bytes instead of characters.
zmodload zsh/system || exit

minlength=11 # length of "#!/bin/bash"
maxlength=1024 # arbitrary here.

ret=0
for file in **/*(N.L+$minlength);do
  if
    sysread -s $maxlength buf < $file &&
      [[ $buf =~ $'(^#![\t ]*((/usr)?/bin/env[ \t]+bash|/bin/bash)([ \t]+-([aCefux]*))?[ \t]*)\n' ]]
  then
    shebang=$match[1] newshebang="#!/bin/sh -$match[5]"
    print -r -- ${(r[$#shebang])newshebang} 1<> $file || ret=$?
  fi
done
exit $ret

Hier sind eine Reihe unterschiedlicher Shebangs mit einer Reihe unterstützter Optionen möglich, die im neuen /bin/shShebang reproduziert und rechts aufgefüllt (mit dem r[length]Parametererweiterungsflag) auf die gleiche Größe wie das Original gebracht werden.

Antwort4

Dateien sind ein langer zusammenhängender Bytebereich. Wenn Sie von bashdurch ersetzen sh, müssen Sie im Wesentlichen die beiden Bytes entfernen (unter der Annahme von UTF-8 oder ähnlich), aus denen besteht ba. Dateien dürfen keine Lücken enthalten, daher shmuss alles ab zwei Bytes früher in die Datei geschrieben werden.

Dies erfordert ein Neuschreiben der gesamten Datei oder zumindest ausgehend vom geänderten Teil.

Es gibt Möglichkeiten,ersetzenBytes in einer Datei, beispielsweise mit unschuldigen Leerzeichen, wenn das Format dies zulässt, ohne die gesamte Datei neu schreiben zu müssen, siehe die akzeptierte Antwort.

verwandte Informationen