Speichern Sie Änderungen an Ort und Stelle mit NON GNU awk

9

Ich bin auf eine Frage (zu SO selbst) gestoßen, bei der OP die Operation bearbeiten und in Input_file (s) selbst speichern muss.

Ich weiß, dass wir für eine einzelne Eingabedatei Folgendes tun können:

awk '{print "test here..new line for saving.."}' Input_file > temp && mv temp Input_file

Nehmen wir nun an, wir müssen Änderungen im gleichen Dateiformat vornehmen (hier wird .txt angenommen).

Was ich für dieses Problem versucht / gedacht habe: Sein Ansatz besteht darin, eine for-Schleife von TXT-Dateien zuawkdurchlaufen,und das Aufrufen von Singleist ein schmerzhafter und NICHT empfohlener Prozess, da unnötige CPU-Zyklen verschwendet werden und für mehr Dateien mehr langsam.

Was könnte hier also möglicherweise getan werden, um eine Inplace-Bearbeitung für mehrere Dateien mit einer NON GNU durchzuführen, awkdie die Inplace-Option nicht unterstützt? Ich habe auch diesen Thread durchgearbeitet. Speichern Sie Änderungen an Ort und Stelle mit awk, aber es gibt nicht viel für NON GNU awk vice und das Ändern mehrerer Dateien an awksich, da ein Nicht-GNU awk keine inplaceOption dazu hat.

HINWEIS: Warum ich einbashTaghinzufüge,da ich in meinem Antwortteil Bash-Befehle verwendet habe, um temporäre Dateien in ihre tatsächlichen Input_file-Namen umzubenennen, um sie hinzuzufügen.



BEARBEITEN: Gemäß dem Kommentar von Ed Sir, der hier ein Beispiel für Beispiele hinzufügt, obwohl der Zweck des Codes dieses Threads auch für die allgemeine Bearbeitung an Ort und Stelle verwendet werden könnte.

Beispiel-Eingabedatei (en):

cat test1.txt
onetwo three
tets testtest

cat test2.txt
onetwo three
tets testtest

cat test3.txt
onetwo three
tets testtest

Beispiel für die erwartete Leistung:

cat test1.txt
1
2

cat test2.txt
1
2

cat test3.txt
1
2
RavinderSingh13
quelle
1
Interessantes und relevantes awk Problem ++
anubhava
1
@ RavinderSingh13 Wenn Sie eine ganze Reihe von Dateien haben, auf die Sie dies anwenden können, verwenden Sie einen einzelnen Aufruf awk(möglicherweise in einer Subshell) oder einer {...}geschlossenen Gruppe und schreiben Sie die Ergebnisse in die gewünschte Ausgabedatei (entweder für jede Eingabedatei). oder eine kombinierte Datei für alle Eingabedateien). Dann leiten Sie einfach die Ausgabe der Unterschale oder der in Klammern eingeschlossenen Gruppe in die aktuelle Datei um, in die geschrieben wird? Das einfache Einfügen einer Zeichenfolge von Eingabedateien nach dem awkBefehl würde alle Dateien (oder ähnliches) nacheinander verarbeiten.
David C. Rankin
@ DavidC.Rankin, danke, dass du darauf geantwortet hast. Ja, ich habe ähnliche Dinge gepostet, die Sie sagen, Sir. Meine Antwort ist auch in dieser Frage enthalten. Ich kenne Ihre Ansichten zu demselben Sir, Prost.
RavinderSingh13
1
Nach einigem Schlafen und Nachdenken sehe ich 2 Optionen (1) mit dem awk {..} file1 .. fileXSchreiben der geänderten Datei als z. B. temp01und in Ihrer nächsten Iteration während der Verarbeitung der nächsten Datei verwenden Sie a mv -f tmp01 input01, um die Eingabedatei mit den geänderten Daten zu überschreiben. oder (2) schreiben Sie einfach ein neues Verzeichnis von ./tmp/tmp01 ... ./tmp/tmp0Xwährend der Ausführung des awkSkripts und folgen Sie mit einer Schleife über die Dateien im ./tmpVerzeichnis und z. B. mv -f "$i" "input_${i##*[^0-9]}"(oder welche Erweiterung Sie benötigen, um die alten Eingabedateien zu ersetzen.
David C. Rankin
@ DavidC.Rankin, vielen Dank, dass Sie Ihre Ansichten hier mitgeteilt haben, Sir. IMHO 1. Option kann ein bisschen riskant sein, da wir etwas ohne awkdie vollständige Code-Vervollständigung tun. Die 2. Option ist fast die gleiche, die ich in meinem Vorschlag verwende Seien Sie dankbar, wenn Sie Ihre Gedanken über diese Lösung wissen lassen könnten, Sir.
RavinderSingh13

Antworten:

6

Da das Hauptziel dieses Threads darin besteht, SAVE in NON GNU zu ersetzen awk, veröffentliche ich zuerst seine Vorlage, die jedem bei jeder Art von Anforderung hilft. Sie müssen ihren Code hinzufügen / anhängen BEGINund einen ENDAbschnitt in ihren Code einfügen, wobei ihr Haupt-BLOCK gemäß ihrem beibehalten wird Anforderung und es sollte dann die Inplace-Bearbeitung durchführen:

HINWEIS: Im Folgenden wird die gesamte Ausgabe in die Ausgabedatei geschrieben. Wenn Sie also etwas in die Standardausgabe drucken möchten, fügen Sie bitte nur eineprint...Anweisung hinzu, ohne die> (out)folgendenSchritte auszuführen.

Generische Vorlage:

awk -v out_file="out" '
FNR==1{
close(out)
out=out_file count++
rename=(rename?rename ORS:"") "mv \047" out "\047 \047" FILENAME "\047"
}
{
    .....your main block code.....
}
END{
 if(rename){
   system(rename)
 }
}
' *.txt


Spezifische Lösung der bereitgestellten Probe:

Ich habe mir einen folgenden Ansatz awkausgedacht (für zusätzliche Beispiele folgt mein Ansatz, um dies zu lösen und die Ausgabe in Input_file selbst zu speichern).

awk -v out_file="out" '
FNR==1{
  close(out)
  out=out_file count++
  rename=(rename?rename ORS:"") "mv \047" out "\047 \047" FILENAME "\047"
}
{
  print FNR > (out)
}
END{
  if(rename){
    system(rename)
  }
}
' *.txt

HINWEIS: Dies ist nur ein Test zum Speichern der bearbeiteten Ausgabe in den Eingabedateien selbst. Sie können den Abschnitt BEGIN zusammen mit dem Abschnitt END in ihrem Programm verwenden. Der Hauptabschnitt sollte den Anforderungen einer bestimmten Frage selbst entsprechen.

Faire Warnung: Da bei diesem Ansatz eine neue temporäre Out-Datei im Pfad erstellt wird, sollten Sie sicherstellen, dass genügend Speicherplatz auf den Systemen vorhanden ist. Letztendlich werden jedoch nur die wichtigsten Eingabedateien gespeichert, während des Betriebs wird jedoch Speicherplatz auf dem System / Verzeichnis benötigt



Es folgt ein Test für den obigen Code.

Programmausführung mit einem Beispiel: Nehmen wir an, es folgen die.txtInput_file (s):

cat << EOF > test1.txt
onetwo three
tets testtest
EOF

cat << EOF > test2.txt
onetwo three
tets testtest
EOF

cat << EOF > test3.txt
onetwo three
tets testtest
EOF

Wenn wir nun folgenden Code ausführen:

awk -v out_file="out" '
FNR==1{
  close(out)
  out=out_file count++
  rename=(rename?rename ORS:"") "mv \047" out "\047 \047" FILENAME "\047"
}
{
  print "new_lines_here...." > (out)
}
END{
  if(rename){
    system("ls -lhtr;" rename)
  }
}
' *.txt

HINWEIS: Ich habeabsichtlich einen Abschnittls -lhtrin densystemAbschnitt eingefügt, um zu sehen, welche Ausgabedateien erstellt werden (temporäre Basis), da diese später in ihren tatsächlichen Namen umbenannt werden.

-rw-r--r-- 1 runner runner  27 Dec  9 05:33 test2.txt
-rw-r--r-- 1 runner runner  27 Dec  9 05:33 test1.txt
-rw-r--r-- 1 runner runner  27 Dec  9 05:33 test3.txt
-rw-r--r-- 1 runner runner  38 Dec  9 05:33 out2
-rw-r--r-- 1 runner runner  38 Dec  9 05:33 out1
-rw-r--r-- 1 runner runner  38 Dec  9 05:33 out0

Wenn ein ls -lhtrAfter- awk Script ausgeführt wird, können nur .txtDateien darin angezeigt werden.

-rw-r--r-- 1 runner runner  27 Dec  9 05:33 test2.txt
-rw-r--r-- 1 runner runner  27 Dec  9 05:33 test1.txt
-rw-r--r-- 1 runner runner  27 Dec  9 05:33 test3.txt


Erläuterung: Fügen Sie hier eine detaillierte Erläuterung des obigen Befehls hinzu:

awk -v out_file="out" '                                    ##Starting awk program from here, creating a variable named out_file whose value SHOULD BE a name of files which are NOT present in our current directory. Basically by this name temporary files will be created which will be later renamed to actual files.
FNR==1{                                                    ##Checking condition if this is very first line of current Input_file then do following.
  close(out)                                               ##Using close function of awk here, because we are putting output to temp files and then renaming them so making sure that we shouldn't get too many files opened error by CLOSING it.
  out=out_file count++                                     ##Creating out variable here, whose value is value of variable out_file(defined in awk -v section) then variable count whose value will be keep increment with 1 whenever cursor comes here.
  rename=(rename?rename ORS:"") "mv \047" out "\047 \047" FILENAME "\047"     ##Creating a variable named rename, whose work is to execute commands(rename ones) once we are done with processing all the Input_file(s), this will be executed in END section.
}                                                          ##Closing BLOCK for FNR==1  condition here.
{                                                          ##Starting main BLOCK from here.
  print "new_lines_here...." > (out)                       ##Doing printing in this example to out file.
}                                                          ##Closing main BLOCK here.
END{                                                       ##Starting END block for this specific program here.
  if(rename){                                              ##Checking condition if rename variable is NOT NULL then do following.
    system(rename)                                         ##Using system command and placing renme variable inside which will actually execute mv commands to rename files from out01 etc to Input_file etc.
  }
}                                                          ##Closing END block of this program here.
' *.txt                                                    ##Mentioning Input_file(s) with their extensions here.
RavinderSingh13
quelle
1
Unterhaltsame Tatsache: Wenn Sie die Eingabedatei im FNR==1Block löschen , können Sie die Änderungen weiterhin an Ort und Stelle speichern. Wie awk 'FNR==1{system("rm " FILENAME)} {print "new lines" > FILENAME}' files.... Dies ist überhaupt nicht zuverlässig (ein vollständiger Datenverlust ist wahrscheinlich), funktioniert aber meistens
einwandfrei
1
Sehr gut erklärte
Problemumgehung
3

Ich würde wahrscheinlich so etwas machen, wenn ich versuchen würde, dies zu tun:

$ cat ../tst.awk
FNR==1 { saveChanges() }
{ print FNR > new }
END { saveChanges() }

function saveChanges(   bak, result, mkBackup, overwriteOrig, rmBackup) {
    if ( new != "" ) {
        bak = old ".bak"
        mkBackup = "cp \047" old "\047 \047" bak "\047; echo \"$?\""
        if ( (mkBackup | getline result) > 0 ) {
            if (result == 0) {
                overwriteOrig = "mv \047" new "\047 \047" old "\047; echo \"$?\""
                if ( (overwriteOrig | getline result) > 0 ) {
                    if (result == 0) {
                        rmBackup = "rm -f \047" bak "\047"
                        system(rmBackup)
                    }
                }
            }
        }
        close(rmBackup)
        close(overwriteOrig)
        close(mkBackup)
    }
    old = FILENAME
    new = FILENAME ".new"
}

$ awk -f ../tst.awk test1.txt test2.txt test3.txt

Ich hätte es vorgezogen, zuerst die Originaldatei in die Sicherung zu kopieren und dann die Änderungen am Original zu speichern, aber dies würde den Wert der Variablen FILENAME für jede Eingabedatei ändern, was unerwünscht ist.

Beachten Sie, dass Sie Originaldateien mit dem Namen whatever.bakoder whatever.newin Ihrem Verzeichnis mit temporären Dateien überschreiben würden, sodass Sie auch dafür einen Test hinzufügen müssten. Ein Aufruf zum mktempAbrufen der temporären Dateinamen wäre robuster.

Die weitaus nützlichere Sache in dieser Situation wäre ein Tool, das jeden anderen Befehl ausführt und den Bearbeitungsteil "Inplace" ausführt, da dies verwendet werden könnte, um "Inplace" -Bearbeitung für POSIX sed, awk, grep, tr, was auch immer und bereitzustellen Sie müssen die Syntax Ihres Skripts nicht print > outjedes Mal ändern, wenn Sie einen Wert drucken möchten. Ein einfaches, fragiles Beispiel:

$ cat inedit
#!/bin/env bash

for (( pos=$#; pos>1; pos-- )); do
    if [[ -f "${!pos}" ]]; then
        filesStartPos="$pos"
    else
        break
    fi
done

files=()
cmd=()
for (( pos=1; pos<=$#; pos++)); do
    arg="${!pos}"
    if (( pos < filesStartPos )); then
        cmd+=( "$arg" )
    else
        files+=( "$arg" )
    fi
done

tmp=$(mktemp)
trap 'rm -f "$tmp"; exit' 0

for file in "${files[@]}"; do
    "${cmd[@]}" "$file" > "$tmp" && mv -- "$tmp" "$file"
done

die Sie wie folgt verwenden würden:

$ awk '{print FNR}' test1.txt test2.txt test3.txt
1
2
1
2
1
2

$ ./inedit awk '{print FNR}' test1.txt test2.txt test3.txt

$ tail test1.txt test2.txt test3.txt
==> test1.txt <==
1
2

==> test2.txt <==
1
2

==> test3.txt <==
1
2

Ein offensichtliches Problem bei diesem ineditSkript ist die Schwierigkeit, die Eingabe- / Ausgabedateien getrennt vom Befehl zu identifizieren, wenn Sie mehrere Eingabedateien haben. Das obige Skript geht davon aus, dass alle Eingabedateien am Ende des Befehls als Liste angezeigt werden und der Befehl einzeln ausgeführt wird. Dies bedeutet jedoch natürlich, dass Sie ihn nicht für Skripte verwenden können, für die zwei oder mehr Dateien erforderlich sind eine Zeit, zB:

awk 'NR==FNR{a[$1];next} $1 in a' file1 file2

oder Skripte, die Variablen zwischen Dateien in der Arg-Liste festlegen, z.

awk '{print $7}' FS=',' file1 FS=':' file2

Machen Sie es robuster als Übung für den Leser, aber sehen Sie sich die xargsZusammenfassung als Ausgangspunkt dafür an, wie ein Robustes ineditfunktionieren müsste :-).

Ed Morton
quelle
0

Die Shell-Lösung ist einfach und wahrscheinlich schnell genug:

for f in *.txt
do  awk '...' $f > $f.tmp
    mv $f.tmp $f
done

Suchen Sie nur dann nach einer anderen Lösung, wenn Sie eindeutig nachgewiesen haben, dass dies zu langsam ist. Denken Sie daran: Vorzeitige Optimierung ist die Wurzel allen Übels.

user448810
quelle
Vielen Dank für Ihre Antwort, aber wie in meiner Frage selbst erwähnt, ist uns diese Antwort bekannt, aber dies ist wirklich ein Overkill bei der Ausführung dieser Aufgabe. Deshalb habe ich erwähnt, ob wir etwas in awk selbst ausprobieren könnten. Vielen Dank für Ihre Zeit und antworten Sie hier Prost.
RavinderSingh13