Guarde las modificaciones en su lugar con NON GNU awk

9

Me he encontrado con una pregunta (sobre SO) donde OP tiene que hacer operaciones de edición y guardado en Input_file (s).

Sé que para un solo Input_file podríamos hacer lo siguiente:

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

Ahora digamos que necesitamos hacer cambios en el mismo tipo de formato de archivos (suponga .txt aquí).

Lo que he intentado / pensado para este problema: su enfoque es pasar por un bucle for de archivos .txt y llamar a singleawkes un proceso doloroso y NO recomendado, ya que desperdiciará ciclos de CPU innecesarios y para una mayor cantidad de archivos sería más lento.

Entonces, ¿qué se podría hacer aquí para realizar la edición in situ para múltiples archivos con un NO GNU awkque no admite la opción in situ. También he revisado este hilo Guarde las modificaciones en el lugar con awk, pero no hay mucho para el vicio de NON GNU awk y el cambio de múltiples archivos in situ dentro de awksí mismo, ya que un no GNU awk no tendrá inplaceopción.

NOTA: ¿Por qué estoy agregandobashetiquetas ya que, en mi parte de respuesta, he usado comandos bash para cambiar el nombre de los archivos temporales a sus nombres reales de Input_file para agregarlos?



EDITAR: Según el comentario de Ed sir agregando un ejemplo de muestras aquí, aunque el propósito del código de este hilo podría usarse también con la edición in situ con fines genéricos.

Muestra de archivos de entrada:

cat test1.txt
onetwo three
tets testtest

cat test2.txt
onetwo three
tets testtest

cat test3.txt
onetwo three
tets testtest

Muestra de salida esperada:

cat test1.txt
1
2

cat test2.txt
1
2

cat test3.txt
1
2
RavinderSingh13
fuente
1
Problema awk interesante y pertinente ++
anubhava
1
@ RavinderSingh13 si tiene una gran cantidad de archivos para aplicar esto, ¿por qué no usar una sola llamada awk(quizás en una subshell) o un {...}grupo cerrado y luego escribir los resultados en el archivo de salida deseado (ya sea para cada archivo de entrada, o un archivo combinado para todos los archivos de entrada). Entonces, ¿simplemente redirige la salida de la subshell o grupo encerrado entre llaves al archivo actual en el que se está escribiendo? ¿Simplemente incluir una cadena de archivos de entrada siguiendo el awkcomando procesará secuencialmente todos los archivos (o algo similar)?
David C. Rankin
@ DavidC.Rankin, gracias por responder a este. Sí, he publicado un tipo similar de cosas que usted dice señor, mi respuesta también se publica en esta pregunta, déjame conocer sus puntos de vista sobre el mismo señor, saludos.
RavinderSingh13
1
Después de dormir un poco y pensar en ello, veo 2 opciones (1) con awk {..} file1 .. fileXescribir el archivo modificado como, por ejemplo, temp01y en su próxima iteración mientras procesa el siguiente archivo, use a mv -f tmp01 input01para sobrescribir el archivo de entrada con los datos modificados; o (2) simplemente escriba un nuevo directorio ./tmp/tmp01 ... ./tmp/tmp0Xdurante la ejecución de la awksecuencia de comandos y haga un seguimiento sobre los archivos del ./tmpdirectorio y, por ejemplo, mv -f "$i" "input_${i##*[^0-9]}"(o cualquier expansión que necesite para reemplazar los archivos de entrada anteriores.
David C. Rankin
@ DavidC.Rankin, Gracias por dejar que sus puntos de vista lo sepan aquí, señor. En mi humilde opinión, la primera opción puede ser un poco arriesgada, ya que estamos haciendo algo sin awkcompletar el código completo, la segunda opción es casi lo mismo que estoy usando en mi sugerencia. Le agradecería si pudiera hacer saber sus pensamientos sobre esa solución, señor.
RavinderSingh13

Respuestas:

6

Dado que el objetivo principal de este hilo es cómo hacer SAVE in-place en NON GNU, awkentonces publico primero su plantilla que ayudará a cualquier persona en cualquier tipo de requisito, necesitan agregar / agregar BEGINy ENDseccionar en su código manteniendo su BLOQUE principal según su requisito y debería hacer la edición in situ y luego:

NOTA: Siguiente escribirá toda su salida en output_file, por lo que en caso de que desee imprimir algo en la salida estándar, solo agregue laprint...declaración sin> (out)seguir.

Plantilla genérica:

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


Solución específica de la muestra proporcionada:

Se me ocurrió el siguiente enfoque dentro de awksí mismo (para las muestras agregadas, el siguiente es mi enfoque para resolver esto y guardar la salida en Input_file)

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: esta es solo una prueba para guardar la salida editada en Input_file (s), uno podría usar su sección BEGIN, junto con su sección END en su programa, la sección principal debe cumplir con el requisito de la pregunta específica en sí.

Advertencia justa: también dado que este enfoque crea un nuevo archivo de salida temporal en la ruta, así que asegúrese de tener suficiente espacio en los sistemas, aunque en el resultado final esto mantendrá solo los Input_file (s) principales, pero durante las operaciones necesita espacio en el sistema / directorio



Lo siguiente es una prueba para el código anterior.

Ejecución del programa con un ejemplo: Supongamos que los siguientes son los.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

Ahora cuando ejecutamos el siguiente código:

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: Tengo lugarls -lhtren lasystemsección intencionalmente para ver qué archivos de salida está creando (de manera temporal) porque más tarde los cambiará a su nombre real.

-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

Cuando hacemos una secuencia de comandos ls -lhtrposterior awka la ejecución, solo podemos ver los .txtarchivos allí.

-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


Explicación: Agregar una explicación detallada del comando anterior aquí:

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
fuente
1
Dato curioso: si elimina el archivo de entrada en FNR==1bloque, aún puede guardar los cambios en el lugar. Al igual awk 'FNR==1{system("rm " FILENAME)} {print "new lines" > FILENAME}' files.... Esto no es confiable en absoluto (es probable que ocurra una pérdida completa de datos), pero aún así, en general funciona bien: D
oguz ismail
1
Muy bien explicado
anubhava
3

Probablemente iría con algo como esto si intentara hacer esto:

$ 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

Hubiera preferido copiar el archivo original a la copia de seguridad primero y luego operar los cambios guardados en el original, pero hacerlo cambiaría el valor de la variable FILENAME para cada archivo de entrada que no es deseable.

Tenga en cuenta que si tuviera un archivo original llamado whatever.bako whatever.newen su directorio, los sobrescribiría con archivos temporales, por lo que también necesitaría agregar una prueba para eso. Una llamada para mktempobtener los nombres de los archivos temporales sería más sólida.

Lo MUCHO más útil de tener en esta situación sería una herramienta que ejecute cualquier otro comando y realice la parte de edición "in situ", ya que podría usarse para proporcionar edición "in situ" para POSIX sed, awk, grep, tr, lo que sea y no requeriría que cambie la sintaxis de su script a print > outetc. cada vez que desee imprimir un valor. Un ejemplo simple y frágil:

$ 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

que usarías de la siguiente manera:

$ 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 problema obvio con ese ineditscript es la dificultad de identificar los archivos de entrada / salida por separado del comando cuando tiene múltiples archivos de entrada. El script anterior asume que todos los archivos de entrada aparecen como una lista al final del comando y el comando se ejecuta contra ellos uno a la vez, pero por supuesto eso significa que no puede usarlo para scripts que requieren 2 o más archivos en un tiempo, por ejemplo:

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

o scripts que establecen variables entre archivos en la lista arg, por ejemplo:

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

Hacerlo más robusto se dejó como un ejercicio para el lector, pero mira la xargssinopsis como un punto de partida de cómo un robusto ineditnecesitaría funcionar :-).

Ed Morton
fuente
0

La solución de shell es simple y probablemente lo suficientemente rápida:

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

Solo busque una solución diferente si ha demostrado de manera concluyente que esto es demasiado lento. Recuerde: la optimización prematura es la raíz de todo mal.

usuario448810
fuente
Gracias por su respuesta, pero como mencioné en mi pregunta, estamos al tanto de esta respuesta, pero esto es realmente una exageración al hacer esta tarea, por eso mencioné si podríamos intentar algo dentro de awk. Gracias por su tiempo y responda aquí saludos.
RavinderSingh13