Comment utiliser regex avec AWK pour le remplacement de chaîne?

13

Supposons qu'il y ait du texte dans un fichier:

(bookmarks
("Chapter 1 Introduction 1" "#1"
("1.1 Problem Statement and Basic Definitions 23" "#2")
("Exercises 31" "#30")
("Notes and References 42" "#34"))
)

Je veux ajouter 11 à chaque numéro suivi d'un "dans chaque ligne s'il y en a un, c'est-à-dire

(bookmarks
("Chapter 1 Introduction 12" "#12"
("1.1 Problem Statement and Basic Definitions 34" "#13")
("Exercises 42" "#41")
("Notes and References 53" "#45"))
)

Voici ma solution en utilisant GNU AWK et regex:

awk -F'#' 'NF>1{gsub(/"(\d+)\""/, "\1+11\"")}'

c'est à dire, je veux remplacer (\d+)\"par \1+10\", où \1est le groupe représentant (\d+). Mais ça ne marche pas. Comment puis-je le faire fonctionner?

Si gawk n'est pas la meilleure solution, quoi d'autre peut être utilisé?

Tim
la source
Désolé pour la duplication. Mais j'ai d'abord demandé sur stackoverflow, et je n'ai obtenu aucune réponse satisfaisante, j'ai donc signalé la migration. Mais cela ne s'est pas produit pendant un certain temps, donc je ne m'attendais pas à ce que cela se produise, puis j'ai demandé sur Unix.SE.
Tim

Réponses:

12

Essayez ceci (gawk est nécessaire).

awk '{a=gensub(/.*#([0-9]+)(\").*/,"\\1","g",$0);if(a~/[0-9]+/) {gsub(/[0-9]+\"/,a+11"\"",$0);}print $0}' YourFile

Testez avec votre exemple:

kent$  echo '(bookmarks
("Chapter 1 Introduction 1" "#1"
("1.1 Problem Statement and Basic Definitions 2" "#2")
("Exercises 30" "#30")
("Notes and References 34" "#34"))
)
'|awk '{a=gensub(/.*#([0-9]+)(\").*/,"\\1","g",$0);if(a~/[0-9]+/) {gsub(/[0-9]+\"/,a+11"\"",$0);}print $0}'   
(bookmarks
("Chapter 1 Introduction 12" "#12"
("1.1 Problem Statement and Basic Definitions 13" "#13")
("Exercises 41" "#41")
("Notes and References 45" "#45"))
)

Notez que cette commande ne fonctionnera pas si les deux nombres (par exemple 1 "et" # 1 ") sont différents. Ou s'il y a plus de nombres sur la même ligne avec ce modèle (par exemple 23" ... 32 "..." # 123 ") sur une seule ligne.


MISE À JOUR

Étant donné que @Tim (OP) a déclaré que le nombre suivi de la "même ligne pourrait être différent, j'ai apporté quelques modifications à ma solution précédente et l'ai fait fonctionner pour votre nouvel exemple.

BTW, d'après l'exemple, je pense que cela pourrait être une table de structure de contenu, donc je ne vois pas comment les deux nombres pourraient être différents. Le premier serait le numéro de page imprimé, et le deuxième avec # serait l'index de la page. Ai-je raison?

Quoi qu'il en soit, vous connaissez le mieux vos besoins. Maintenant la nouvelle solution, toujours avec gawk (je décompose la commande en lignes pour en faciliter la lecture):

awk 'BEGIN{FS=OFS="\" \"#"}{if(NF<2){print;next;}
        a=gensub(/.* ([0-9]+)$/,"\\1","g",$1);
        b=gensub(/([0-9]+)\"/,"\\1","g",$2); 
        gsub(/[0-9]+$/,a+11,$1);
        gsub(/^[0-9]+/,b+11,$2);
        print $1,$2
}' yourFile

tester avec votre nouvel exemple:

kent$  echo '(bookmarks
("Chapter 1 Introduction 1" "#1"
("1.1 Problem Statement and Basic Definitions 23" "#2")
("Exercises 31" "#30")
("Notes and References 42" "#34"))
)
'|awk 'BEGIN{FS=OFS="\" \"#"}{if(NF<2){print;next;}
        a=gensub(/.* ([0-9]+)$/,"\\1","g",$1);
        b=gensub(/([0-9]+)\"/,"\\1","g",$2); 
        gsub(/[0-9]+$/,a+11,$1);
        gsub(/^[0-9]+/,b+11,$2);
        print $1,$2
}'                        
(bookmarks
("Chapter 1 Introduction 12" "#12"
("1.1 Problem Statement and Basic Definitions 34" "#13")
("Exercises 42" "#41")
("Notes and References 53" "#45"))
)


EDIT2 basé sur le commentaire de @Tim

(1) FS = OFS = "\" \ "#" signifie-t-il que le séparateur de champ en entrée et en sortie est un guillemet double, un espace, un guillemet double et #? Pourquoi spécifier deux fois le guillemet double?

Vous avez raison pour le séparateur à la fois en entrée et en sortie. Il définit le séparateur comme:

" "#

Il y a deux guillemets doubles, car il est plus facile d'attraper les deux nombres que vous souhaitez (en fonction de votre exemple d'entrée).

(2) Dans /.* ([0-9] +) $ /, $ signifie-t-il la fin de la chaîne?

Exactement!

(3) Dans le troisième argument de gensub (), quelle est la différence entre "g" et "G"? il n'y a pas de différence entre G et g. Regarde ça:

gensub(regexp, replacement, how [, target]) #
    Search the target string target for matches of the regular expression regexp. 
    If "how" is a string beginning with g or G (short for global”), then 
        replace all matches of regexp with replacement.

Il s'agit de http://www.gnu.org/s/gawk/manual/html_node/String-Functions.html . vous pouvez lire pour obtenir une utilisation détaillée de gensub.

Kent
la source
Merci! Je me demande comment le faire fonctionner si les deux nombres, par exemple 1 "et" # 1 "sont différents?
Tim
cette réponse fonctionne pour votre exigence / exemple actuel. si l'exigence est modifiée, vous pouvez peut-être modifier la question et donner un meilleur exemple. et à partir de votre code awk -F'#', il semble que vous ne souhaitiez effectuer la modification sur la pièce qu'après le '#'?
Kent
Merci pour votre suggestion. Je viens de modifier mon exemple pour que les deux nombres ne soient pas identiques.
Tim
@Tim voir ma réponse mise à jour, pour votre nouvel exemple.
Kent
Merci! Quelques questions: (1) FS=OFS="\" \"#"signifie que le séparateur de champ à la fois en entrée et en sortie est guillemet double, espace, guillemet double et #? pourquoi spécifier deux fois le guillemet double? (2) dans /.* ([0-9]+)$/, $signifie la fin de la chaîne? (3) dans le troisième argument de gensub (), quelle est la différence entre "g"et "G"?
Tim
7

Contrairement à presque tous les outils qui fournissent des substitutions d'expression rationnelle, awk n'autorise pas les références arrières, comme \1dans le texte de remplacement. GNU Awk donne accès aux groupes correspondants si vous utilisez la matchfonction , mais pas avec ~ou subou gsub.

Notez également que même s'il \1était pris en charge, votre extrait de code ajouterait la chaîne +11, sans effectuer de calcul numérique. De plus, votre expression rationnelle n'est pas tout à fait juste, vous faites correspondre des choses comme "42""et non "#42".

Voici une solution awk (avertissement, non testé). Il n'effectue qu'un seul remplacement par ligne.

awk '
  match($0, /"#[0-9]+"/) {
    n = substr($0, RSTART+2, RLENGTH-3) + 11;
    $0 = substr($0, 1, RSTART+1) n substr($0, RSTART+RLENGTH-1)
  }
  1 {print}'

Ce serait plus simple en Perl.

perl -pe 's/(?<="#)[0-9]+(?=")/$1+11/e'
Gilles 'SO- arrête d'être méchant'
la source
La première phrase de votre réponse est exactement ce que je cherchais. Cependant, le fait que vous ayez dit "... dans le texte de remplacement" soulève une question complémentaire: awk autorise-t-il les références arrières dans le modèle d'expression régulière lui-même?
Wildcard
1
@Wildcard Non, awk ne tient tout simplement pas compte des groupes (à l'exception de l'extension GNU que je mentionne).
Gilles 'SO- arrête d'être méchant'
5

awkpeut le faire, mais ce n'est pas direct, même en utilisant le renvoi.
GNU awk a une rétro-référence (partielle), sous forme de gensub .

Les instances de 123"sont temporairement enveloppées \x01et \x02pour les marquer comme non modifiées (poursub() . Co

Ou vous pouvez simplement parcourir les candidats qui changent de boucle au fur et à mesure, auquel cas, la référence arrière et les "crochets" ne sont pas nécessaires; mais le suivi de l'index des caractères est nécessaire.

awk '{$0=gensub(/([0-9]+)\"/, "\x01\\1\"\x02", "g", $0 )
      while ( match($0, /\x01[0-9]+\"\x02/) ) {
        temp=substr( $0, RSTART, RLENGTH )
        numb=substr( temp, 2, RLENGTH-3 ) + 11
        sub( /\x01[0-9]+\"\x02/, numb "\"" ) 
      } print }'

Voici une autre manière, en utilisant gensuband array splitet \x01comme délimiteur de champ (pour split ) .. \ x02 marque un élément de tableau comme candidat pour l'addition arithmétique.

awk 'BEGIN{ ORS="" } {
     $0=gensub(/([0-9]+)\"/, "\x01\x02\\1\x01\"", "g", $0 )
     split( $0, a, "\x01" )
     for (i=0; i<length(a); i++) { 
       if( substr(a[i],1,1)=="\x02" ) { a[i]=substr(a[i],2) + 11 }
       print a[i]
     } print "\n" }'
Peter.O
la source
Merci! Dans votre premier code, (1) qu'est-ce que cela "\x01\\1\"\x02"signifie? Je ne comprends toujours pas \x01et \x02. (2) Quelle est la différence entre le retour $0par gensubet $0le dernier argument gensub?
Tim
@Tim. Les valeurs hexadécimales \x01et \x02sont utilisées comme marqueurs de substitution. Ces valeurs sont très peu susceptibles d'être dans une normale fichier texte, ils sont également « très » en sécurité à utiliser (ie. Rencontre pas d' un affrontement avec les pré-existants) .. Ils ne sont que des étiquettes temporaires .. Re $0=gensub(... $0).. voir ce lien String-Manipulation Functions , mais en résumé: It (gensub) renvoie la chaîne modifiée comme résultat de la fonction et la chaîne cible d'origine n'est pas modifiée. ... Le $0=modifie simplement la cible d'origine ..
Peter.O
2

Comme les solutions en (g) awk semblent devenir assez complexes, je voulais ajouter une solution alternative en Perl:

perl -wpe 's/\d+(?=")/$&+11/eg' < in.txt > out.txt

Explication:

  • L'option -wactive les avertissements (qui vous avertiront d'éventuels effets indésirables).
  • Option -pimplique une boucle autour du code qui fonctionne de manière similaire à sed ou awk, sauver chaque ligne d'entrée automatiquement dans la variable par défaut, $_.
  • L'option -eindique à perl que le code du programme suit sur la ligne de commande, pas dans un fichier de script.
  • Le code est une substitution regex ( s/.../.../) activée $_, où une séquence de chiffres, si elle est suivie d'un ", sera remplacée par la séquence, interprétée comme un nombre dans l'addition, plus 11.
  • L' assertion d' (?=pattern) anticipation positive de largeur nulle recherche le "sans le prendre en compte, nous n'avons donc pas à le répéter dans le remplacement. La variable MATCH $&dans le remplacement ne contiendra alors que le nombre.
  • Le /emodificateur de l'expression régulière indique perl«d'exécuter» le remplacement comme code au lieu de le prendre comme chaîne.
  • Le /gmodificateur rend le remplacement "global", en le répétant à chaque match de la ligne.

La variable MATCH $&sera malheureusement préjudiciable aux performances du code dans les versions Perl antérieures à 5.20. Une solution plus rapide (et pas beaucoup plus complexe) utiliserait $1plutôt le regroupement et la référence arrière :

perl -wpe 's/(\d+)?="/$1+11/eg' < in.txt > out.txt

Et si l'assertion prospective semble trop déroutante, vous pouvez également remplacer explicitement le guillemet:

perl -wpe 's/(\d+)"/$1+11 . q{"}/eg' < in.txt > out.txt
Dubu
la source