Enregistrer les modifications en place avec NON GNU awk

9

Je suis tombé sur une question (sur SO lui-même) où OP doit faire l'opération d'édition et de sauvegarde dans Input_file (s) lui-même.

Je sais que pour un seul fichier d'entrée, nous pourrions faire ce qui suit:

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

Supposons maintenant que nous devons apporter des modifications dans le même type de format de fichiers (supposons .txt ici).

Ce que j'ai essayé / pensé pour ce problème: Son approche passe par une boucle for de fichiers .txt et appeler singleawkest un processus douloureux et NON recommandé, car cela gaspillera des cycles de processeur inutiles et pour plus de fichiers, ce serait plus lent.

Donc, que pourrait-on faire ici pour effectuer une modification inplace pour plusieurs fichiers avec un NON GNU awkqui ne prend pas en charge l'option inplace. J'ai également parcouru ce fil Enregistrer les modifications en place avec awk mais il n'y a pas grand-chose pour NON GNU awk vice et changer plusieurs fichiers en awklui-même, car un awk non GNU n'aura pas d' inplaceoption.

REMARQUE: Pourquoi j'ajoute unebashbalise depuis, dans ma partie réponse, j'ai utilisé des commandes bash pour renommer les fichiers temporaires en leurs noms de fichier d'entrée afin de les ajouter.



EDIT: Selon le commentaire d'Ed Sir, en ajoutant un exemple d'échantillons ici, bien que le but du code de ce fil puisse également être utilisé par l'édition générique sur place.

Exemple de fichier (s) d'entrée:

cat test1.txt
onetwo three
tets testtest

cat test2.txt
onetwo three
tets testtest

cat test3.txt
onetwo three
tets testtest

Exemple de sortie attendue:

cat test1.txt
1
2

cat test2.txt
1
2

cat test3.txt
1
2
RavinderSingh13
la source
1
Problème awk intéressant et pertinent ++
anubhava
1
@ RavinderSingh13 si vous avez tout un tas de fichiers à appliquer, pourquoi ne pas utiliser un seul appel à awk, (peut-être dans un sous-shell) ou un {...}groupe inclus, puis écrire les résultats dans le fichier de sortie souhaité (soit pour chaque fichier d'entrée, ou un fichier combiné pour tous les fichiers d'entrée). Ensuite, vous redirigez simplement la sortie du groupe sous-shell ou accolade vers le fichier en cours d'écriture? Le simple fait d'inclure une chaîne de fichiers d'entrée à la suite de la awkcommande traiterait séquentiellement tous les fichiers (ou quelque chose de similaire) ??
David C. Rankin
@ DavidC.Rankin, merci d'avoir répondu à celui-ci. Ouais, j'ai posté le même genre de choses que vous dites monsieur, ma réponse est également publiée dans cette question, laissez-moi savoir votre point de vue sur le même monsieur, cheers.
RavinderSingh13
1
Après un peu de sommeil et d'y penser, je vois 2 options (1) avec awk {..} file1 .. fileXécrire le fichier modifié comme, par exemple temp01et dans votre prochaine itération lors du traitement du fichier suivant, utilisez un mv -f tmp01 input01pour écraser le fichier d'entrée avec les données modifiées; ou (2) il suffit d'écrire un nouveau répertoire de ./tmp/tmp01 ... ./tmp/tmp0Xpendant l'exécution du awkscript et de suivre avec une boucle sur les fichiers dans le ./tmprépertoire et, par exemple mv -f "$i" "input_${i##*[^0-9]}"(ou toute autre extension dont vous avez besoin pour remplacer les anciens fichiers d'entrée.
David C. Rankin
@ DavidC.Rankin, Merci d'avoir fait savoir votre point de vue ici, à mon humble avis, la première option peut être un peu risquée, car nous faisons quelque chose sans awkl'achèvement complet du code, la deuxième option est presque la même que celle que j'utilise dans ma suggestion. soyez reconnaissant si vous pouviez faire part de vos réflexions sur cette solution, monsieur.
RavinderSingh13

Réponses:

6

Étant donné que l'objectif principal de ce fil est de savoir comment faire pour enregistrer en place dans NON GNU awk, je publie d' abord son modèle qui aidera toute personne dans n'importe quel type d'exigence, ils doivent ajouter / ajouter BEGINet ENDsectionner dans leur code en gardant leur BLOC principal conformément à leur exigence et il devrait alors effectuer la modification sur place:

REMARQUE: ce qui suit écrira toute sa sortie dans le fichier de sortie, donc si vous souhaitez imprimer quoi que ce soit sur la sortie standard, veuillez uniquement ajouter uneprint...instruction sans> (out)suivre.

Modèle générique:

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


Solution d'échantillon fournie spécifique:

J'ai trouvé l'approche suivante en awkelle-même (pour les échantillons ajoutés, voici mon approche pour résoudre ce problème et enregistrer la sortie dans Input_file lui-même)

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

REMARQUE: ceci n'est qu'un test pour enregistrer la sortie éditée dans Input_file (s) lui-même, on pourrait utiliser sa section BEGIN, ainsi que sa section END dans leur programme, la section principale devrait être conforme aux exigences de la question spécifique elle-même.

Juste avertissement: Étant donné que cette approche crée un nouveau fichier de sortie temporaire dans le chemin, assurez-vous qu'il y a suffisamment d'espace sur les systèmes, mais au final, cela ne gardera que les fichiers d'entrée principaux, mais pendant les opérations, il a besoin d'espace sur le système / répertoire



Voici un test pour le code ci-dessus.

Exécution du programme avec un exemple: Supposons que les.txtfichiers d'entréesont lessuivants:

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

Maintenant, lorsque nous exécutons le code suivant:

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

REMARQUE: j'ai une placels -lhtrdanssystem section intentionnellement pour voir quels fichiers de sortie il crée (base temporaire) car plus tard, il les renommera en leur nom réel.

-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

Lorsque nous exécutons un script ls -lhtraprès l' awkexécution, nous ne pouvons y voir que des .txtfichiers.

-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


Explication: Ajout d'une explication détaillée de la commande ci-dessus ici:

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
la source
1
Fait amusant: si vous supprimez le fichier d'entrée en FNR==1bloc, vous pouvez toujours enregistrer les modifications sur place. Comme awk 'FNR==1{system("rm " FILENAME)} {print "new lines" > FILENAME}' files.... Ce n'est pas fiable du tout (une perte de données complète est susceptible de se produire), mais cela fonctionne généralement bien: D
oguz ismail
1
Contournement
3

J'irais probablement avec quelque chose comme ça si j'essayais de faire ça:

$ 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

J'aurais préféré copier le fichier d'origine dans la sauvegarde d'abord, puis opérer sur les modifications de sauvegarde de l'original, mais cela changerait la valeur de la variable FILENAME pour chaque fichier d'entrée, ce qui n'est pas souhaitable.

Notez que si vous aviez un fichier original nommé whatever.bak ou whatever.newdans votre répertoire, vous devez le remplacer par des fichiers temporaires, vous devez donc également ajouter un test pour cela. Un appel à mktemppour obtenir les noms des fichiers temporaires serait plus robuste.

La chose la plus utile à avoir dans cette situation serait un outil qui exécute toute autre commande et effectue la partie d'édition "sur place" car cela pourrait être utilisé pour fournir une édition "sur place" pour POSIX sed, awk, grep, tr, peu importe et ne vous obligerait pas à changer la syntaxe de votre script en print > outetc. à chaque fois que vous souhaitez imprimer une valeur. Un exemple simple et 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

que vous utiliseriez comme suit:

$ 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 problème évident avec cela inedit script est la difficulté d'identifier les fichiers d'entrée / sortie séparément de la commande lorsque vous avez plusieurs fichiers d'entrée. Le script ci-dessus suppose que tous les fichiers d'entrée apparaissent sous forme de liste à la fin de la commande et la commande est exécutée contre eux un par un, mais bien sûr cela signifie que vous ne pouvez pas l'utiliser pour des scripts qui nécessitent 2 fichiers ou plus à une heure, par exemple:

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

ou des scripts qui définissent des variables entre les fichiers de la liste arg, par exemple:

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

Le rendre plus robuste à gauche comme exercice pour le lecteur, mais regardez le xargssynopsis comme point de départ pour savoir comment un robuste ineditdevrait fonctionner :-).

Ed Morton
la source
0

La solution shell est simple et probablement assez rapide:

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

Ne recherchez une solution différente que si vous avez démontré de manière concluante qu'elle est trop lente. Rappelez-vous: l'optimisation prématurée est la racine de tout mal.

user448810
la source
Merci pour votre réponse, mais comme mentionné dans ma question elle-même, nous sommes conscients de cette réponse, mais c'est vraiment une exagération de faire cette tâche, c'est pourquoi j'ai mentionné si nous pouvions essayer quelque chose dans awk lui-même. Merci pour votre temps et répondez ici cheers.
RavinderSingh13