Salvare le modifiche in atto con NON GNU awk


9

Mi sono imbattuto in una domanda (su SO stesso) in cui OP deve eseguire le operazioni di modifica e salvataggio in Input_file (s) stesso.

So che per un singolo Input_file potremmo fare quanto segue:

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

Ora supponiamo che dobbiamo apportare modifiche nello stesso tipo di formato di file (supponiamo qui .txt).

Cosa ho provato / pensato per questo problema: il suo approccio sta attraversando un ciclo for di file .txt e chiamare singleawkè un processo doloroso e NON raccomandato, poiché sprecherà cicli inutili di CPU e per un numero maggiore di file sarebbe più lento.

Quindi cosa potrebbe essere fatto qui per eseguire la modifica inplace per più file con un NON GNU awkche non supporta l'opzione inplace. Ho anche attraversato questo thread Salva le modifiche in atto con awk ma non c'è molto per il vice awk NON GNU e la modifica di più file all'interno di awkse stesso, poiché un awk non GNU non avrà alcuna inplaceopzione.

NOTA: Perché sto aggiungendo unbashtag poiché, nella mia parte di risposta, ho usato i comandi bash per rinominare i file temporanei con i loro nomi effettivi Input_file, quindi aggiungendolo.



EDIT: Secondo il commento di Ed sir che aggiunge un esempio di esempi qui, sebbene lo scopo del codice di questo thread possa essere usato anche per scopi generici di modifica sul posto.

File di input di esempio:

cat test1.txt
onetwo three
tets testtest

cat test2.txt
onetwo three
tets testtest

cat test3.txt
onetwo three
tets testtest

Esempio di output previsto:

cat test1.txt
1
2

cat test2.txt
1
2

cat test3.txt
1
2

1
Problema awk interessante e pertinente ++
anubhava

1
@ RavinderSingh13 se hai un sacco di file a cui applicare questo, perché non usare una singola chiamata a awk, (forse in una subshell) o in un {...}gruppo chiuso e quindi scrivere i risultati nel file di output desiderato (sia per ogni file di input, o un file combinato per tutti i file di input). Quindi reindirizzi semplicemente l'output della subshell o del gruppo racchiuso tra parentesi graffe al file corrente su cui stai scrivendo? Includere semplicemente una stringa di file di input a seguito del awkcomando elabora in sequenza tutti i file (o qualcosa di simile) ??
David C. Rankin il

@ DavidC.Rankin, grazie per aver risposto su questo. Sì, ho pubblicato un tipo simile di cose che dici, signore, la mia risposta è pubblicata anche in questa domanda, fammi sapere le tue opinioni sullo stesso signore, salute.
RavinderSingh13,

1
Dopo un po 'di sonno e pensandoci, vedo 2 opzioni (1) con awk {..} file1 .. fileXscrivere il file modificato come, ad estemp01 e nella prossima iterazione durante l'elaborazione del file successivo, usare a mv -f tmp01 input01per sovrascrivere il file di input con i dati modificati; oppure (2) scrivere semplicemente una nuova directory ./tmp/tmp01 ... ./tmp/tmp0Xdurante l'esecuzione dello awkscript e seguire con un ciclo i file nella ./tmpdirectory e, ad esempio mv -f "$i" "input_${i##*[^0-9]}"(o qualunque espansione sia necessaria per sostituire i vecchi file di input.
David C. Rankin

@ DavidC.Rankin, Grazie per aver reso noto il tuo punto di vista, signore, la prima opzione di IMHO potrebbe essere un po 'rischiosa, dal momento che stiamo facendo qualcosa senza awkil completamento completo del codice, la seconda opzione è quasi la stessa che sto usando nel mio suggerimento le sarei grato se potesse far conoscere i tuoi pensieri su quella soluzione, signore.
RavinderSingh13,

Risposte:


6

Poiché lo scopo principale di questa discussione è come fare SAVE in posto in NON GNU, awkquindi sto pubblicando prima il suo modello che aiuterà chiunque in qualsiasi tipo di esigenza, hanno bisogno di aggiungere / aggiungere BEGINe ENDsezione nel loro codice mantenendo il loro BLOCCO principale secondo il loro requisito e dovrebbe eseguire la modifica sul posto quindi:

NOTA: In seguito scriverò tutto il suo output su output_file, quindi nel caso in cui si desideri stampare qualcosa sull'output standard, si prega di aggiungere solo l'print...istruzione senza> (out)seguire.

Modello generico:

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


Soluzione di esempio specifica fornita:

Ho escogitato il seguente approccio al awksuo interno (per esempi aggiunti di seguito è il mio approccio per risolvere questo problema e salvare l'output in Input_file stesso)

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

NOTA: questo è solo un test per salvare l'output modificato in Input_file (s) stesso, si potrebbe usare la sua sezione BEGIN, insieme alla sua sezione END nel loro programma, la sezione principale dovrebbe essere secondo il requisito della domanda specifica stessa.

Avviso equo: anche dal momento che questo approccio crea un nuovo file temporaneo in uscita, quindi meglio assicurarsi di avere abbastanza spazio sui sistemi, anche se alla fine questo manterrà solo i file di input principali, ma durante le operazioni ha bisogno di spazio sul sistema / directory



Di seguito è riportato un test per il codice sopra.

Esecuzione del programma con un esempio: supponiamo che i seguenti file siano .txtInput_file:

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

Ora quando eseguiamo il seguente codice:

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

NOTA: ho un postols -lhtrsystem intenzionalmente nella sezione per vedere quali file di output sta creando (base temporanea) perché in seguito li rinominerà nel loro nome reale.

-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

Quando eseguiamo uno script ls -lhtrafter awkcon l'esecuzione, possiamo vedere solo i .txtfile lì dentro.

-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


Spiegazione: Aggiunta di una spiegazione dettagliata del comando sopra qui:

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.

1
Curiosità: se si elimina il file di input in FNR==1blocco, è comunque possibile salvare le modifiche sul posto. Come awk 'FNR==1{system("rm " FILENAME)} {print "new lines" > FILENAME}' files.... Questo non è affatto affidabile (è probabile che si verifichi una perdita completa di dati), ma comunque funziona per lo più bene: D
Oguz Ismail


3

Probabilmente andrei con qualcosa del genere se dovessi provare a fare questo:

$ 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

Avrei preferito prima copiare il file originale sul backup e poi operare su quel salvataggio delle modifiche all'originale, ma così facendo cambierei il valore della variabile FILENAME per ogni file di input che è indesiderabile.

Nota che se avessi un file originale chiamato whatever.bako whatever.newnella tua directory, li sovrascriveresti con i file temporanei, quindi dovresti aggiungere un test anche per quello. Una chiamata amktemp ottenere i nomi dei file temporanei sarebbe più robusta.

La cosa PIÙ utile da avere in questa situazione sarebbe uno strumento che esegue qualsiasi altro comando e fa la parte di editing "inplace" poiché potrebbe essere usata per fornire l'editing "inplace" per POSIX sed, awk, grep, tr, qualunque cosa e non richiederebbe di modificare la sintassi dello script in print > outecc. ogni volta che si desidera stampare un valore. Un esempio semplice, fragile:

$ 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

che useresti come segue:

$ 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

Un ovvio problema con quello ineditscript è la difficoltà di identificare i file di input / output separatamente dal comando quando si hanno più file di input. Lo script sopra presuppone che tutti i file di input vengano visualizzati come un elenco alla fine del comando e il comando viene eseguito uno contro l'altro alla volta, ma ovviamente ciò significa che non è possibile utilizzarlo per gli script che richiedono 2 o più file in una volta, ad esempio:

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

o script che impostano variabili tra i file nell'elenco arg, ad esempio:

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

Rendendolo più robusto lasciato come esercizio per il lettore, ma guarda alla xargssinossi come punto di partenza per come un robusto ineditdovrebbe funzionare :-).


0

La soluzione shell è semplice e probabilmente abbastanza veloce:

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

Cerca una soluzione diversa solo se hai definitivamente dimostrato che è troppo lento. Ricorda: l'ottimizzazione prematura è la radice di tutti i mali.


Grazie per la tua risposta, ma come menzionato nella mia stessa domanda, siamo consapevoli di questa risposta, ma questo è davvero un eccesso di fare questo compito, ecco perché ho menzionato se potessimo provare qualcosa all'interno di Awk stesso. Grazie per il tuo tempo e rispondi qui evviva.
RavinderSingh13
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.