1
votes

RegEx pour remplacer uniquement à gauche des commentaires LaTeX

Le fichier que je transforme (LaTeX) contient des commentaires, qui se trouvent à droite d'un%. Tout signe de pourcentage non échappé marque un commentaire.

En utilisant perl, je souhaite effectuer des substitutions de regex, disons

bash: cat aaa
dog % cat
dog \% cat
bash: cat aaa | perl -n -e 'use strict; use warnings; print if (m/(?<!\x5c)%/)'
dog % cat
bash: 

mais uniquement dans du texte non commenté. Ainsi les lignes

Un chien a mangé un rat mais 5 \% des chiens ont mangé la pomme% dog ??

Mon chien est plus intelligent que votre élève d’honneur

serait transformé en

Un CHAT a mangé un rat mais 5 \% des CHAT ont mangé la pomme% dog ??

Mon CAT est plus intelligent que votre élève d’honneur

Voici, bien sûr, comment faire correspondre un signe de pourcentage non échappé:

s/dog/CAT/g

Cela doit être une question bien connue mais je ne l'ai pas fait trouvez les bons termes de recherche pour trouver la réponse. Ne peut-on pas faire cela en perl avec une seule regex? De toute évidence, mon regex de substitution remplacerait chaque chien par CAT , même dans les commentaires.


0 commentaires

4 Réponses :


2
votes

Une façon: extraire tout le texte jusqu'à (sans échappement) % puis exécuter le remplacement dans ce

One CAT 5\% of CATs % dog
%dog more than 10\% of % dogs
CATs \% and CATs

Le modificateur / e fait le côté de remplacement soit évalué comme du code, et nous y exécutons une expression régulière.

Là, nous devons d'abord enregistrer le «reste» de la ligne (après % ), capturé dans $ 2 , puisque $ 2 sera effacé dans la regex à venir.

Le modificateur / r dans cette expression régulière lui permet de renvoyer la chaîne transformée, pratique pour former la valeur à utiliser comme remplacement (en la concaténant avec le reste de la ligne) . De plus, avoir l'original inchangé sous / r nous permet d'utiliser la substitution sur $ 1 (qui est en lecture seule).


Le [^ \\] ci-dessus nécessite un caractère autre que \ pour précéder % , pour que le commentaire commence. Cependant, comme il requiert un caractère, il fait correspondre l'ensemble de l'expression régulière si la ligne commence par % et a en outre % non échappé, ce qui est faux. C'est tout à fait possible: une ligne contient du texte commenté (% ... ), et à un moment donné, elle est également complètement commentée.

Si c'est effectivement un problème, utilisez un Lookahead négatif à la place

perl -nwe'
    s{ (.*?) ((?<!\\)%.*)? $}{$r=($2//""); $1=~s{dog}{CAT}gr . $r}egx; print
' data.txt

Notez que le retour en arrière (nécessaire) dans cette situation nuit à l'efficacité. Cela ne devrait pas être un problème pour un fichier Latex occasionnel, mais si cela est fait beaucoup , cela peut l'être. Dans ce cas, analysez chaque ligne correctement, de sorte que les recherches ne seront pas nécessaires.

Test, avec le fichier d'entrée data.txt

One dog 5\% of dogs % dog
%dog more than 10\% of % dogs
dogs \% and dogs

Le one-liner

s{ (.*?) ((?<!\\)%.*)? $ }{ $r=($2//''); $1=~s{dog}{CAT}gr . $r}egx;

prints

s/ (.*?) ([^\\]%.*) /$r=$2; $1=~s{dog}{CAT}gr . $r/egx;


0 commentaires

1
votes

Il peut être plus simple de le diviser en deux tâches: trouver la partie de la chaîne qui n'est pas un commentaire, puis faire votre substitution sur cette partie. Voici une approche pour cela:

my $escaping;
while ($str =~ m/\G((\\+)|(%)|[^\\%]+)/g) {
  my ($token, $backslashes, $percent) = ($1, $2, $3);
  $in_comment = 1 if defined $percent and !$escaping;
  $escaping = (defined $backslashes and length($backslashes) % 2) ? 1 : 0;

Ceci utilise un negatif lookbehind pour trouver le premier signe de pourcentage non échappé, même s'il s'agit du premier caractère de la chaîne, et rend le commentaire à moitié facultatif afin qu'il se substitue toujours s'il n'y a pas de commentaire. Cependant, cela impliquera encore beaucoup de backtracking donc si les performances sont un problème, une implémentation plus étendue peut être préférable .

EDIT: La raison pour laquelle cela semble si complexe est que vous essayez de faire quelque chose pour lequel les regex ne sont pas vraiment géniaux. Vous souhaitez rechercher des éléments dans une chaîne en fonction de l'état contextuel. La "meilleure" façon de faire est d'analyser la chaîne en jetons, ce qui est généralement fait avec une boucle qui garde l'état et une regex (ce qui est bon dans cette partie); même s'il ne s'agit que de jetons de "chaîne sans commentaire", "début de commentaire", "chaîne de commentaire". Ensuite, vous pouvez facilement opérer uniquement sur les chaînes sans commentaires.

Voici à quoi pourrait ressembler un algorithme étendu, j'ai essayé de le simplifier à la quantité d'analyse nécessaire pour ce cas et il pourrait certainement être approfondi. La clé est d'utiliser m / \ G ... / g pour analyser la chaîne de manière incrémentielle ( \ G ancre la correspondance à la fin de la dernière correspondance avec le / g dans un contexte scalaire), et comptez sur le moteur d'expression régulière pour choisir la première option d'alternance qui correspond à ce point dans la chaîne. De cette façon, vous parcourez la chaîne de manière séquentielle sans retour en arrière, et gardez l'état en dehors de la boucle.

use strict;
use warnings;
my $str = 'One dog ate a rat but 5\% of dogs ate the apple % dog??';
my $in_comment;
my ($text, $comment) = ('','');
while ($str =~ m/\G(((?<!\\)%)|%|[^%]+)/g) {
  my ($token, $start_comment) = ($1, $2);
  $in_comment = 1 if defined $start_comment;
  if ($in_comment) {
    $comment .= $token;
  } else {
    $text .= $token;
  }
}
$text =~ s/dog/CAT/g;
$str = "$text$comment";

Voici une approche de tokenisation différente qui vous permet de gérer les contre-obliques d'échappement, si cela est autorisé, en gardant une trace de si le jeton suivant est échappé:

use strict;
use warnings;
my $str = 'One dog ate a rat but 5\% of dogs ate the apple % dog??';
if (my ($first, $second) = $str =~ m/\A(.*?)((?<!\\)%.*)?\z/s) {
  $first =~ s/dog/CAT/g;
  $str = defined $second ? "$first$second" : $first;
}

Parser :: MGC est une abstraction de ce concept à une interface objet.

(Aussi: cette méthode ne sera pas toujours plus rapide qu'une seule regex de retour en arrière, en particulier avec une analyse plus simple et des lignes plus courtes.)


2 commentaires

Pourriez-vous donner un exemple de la meilleure façon d'analyser la chaîne en jetons? Et pourrait-il être écrit comme un script perl qui opère sur un fichier? Avec le séparateur d'enregistrement d'entrée par défaut, cela fonctionnerait sur une ligne à la fois, et bien sûr, il n'y aurait pas de définition codée en dur de $ str dans le code.


@JacobWegelin J'ai ajouté un exemple. Ce concept ne correspond probablement pas bien à une ligne unique, mais il pourrait certainement être utilisé dans un script perl comme vous le décrivez.



0
votes

Une solution plus prolixe et détaillée, basée sur zdim:

bash: cat aaa
dog and dogs and many many dogs% dog
dog and dogs and many many dogs\% dog
bash: cat aaa | perl -n -e 'use strict; use warnings; my $r; s/ (.*?) ((?<!\x5c)%.*) /$r=$2; $1=~s{dog}{CAT}gr . $r/egx; print;'
CAT and CATs and many many CATs% dog
dog and dogs and many many dogs\% dog 

Notez que cela permet un marqueur de commentaire immédiatement après le texte sans commentaire; il ne nécessite pas d'espace pour précéder le%.


2 commentaires

(1) " il ne nécessite pas d'espace pour précéder le % " - si cela fait référence à la réponse citée (la mienne) alors notez que dans ma réponse aucun espace n'est obligatoire. L'espace qui était là n'était pas une partie du modèle mais pour la lisibilité, par / x (également supprimé maintenant). (2) Cela ne fonctionne pas - pas de remplacements sur la deuxième ligne?


(les vérifications peuvent être délicates. voir aussi ma réponse mise à jour)



0
votes
#!/usr/bin/perl
# Default input record separator: one line at a time.
# Read through a LaTeX file line by line. Distinguish comment from text.
# Parse each line into exactly 2 tokens. 
# Boundary between tokens is the first non-escaped %.
# $text: everything up to, but excluding, boundary if exists; else entire line.
# $comment: possibly null, from the first non-escaped % to end of line. 
# Last (pathological) line might not end in LF, hence LF is excluded from tokens and appended at the end.
# Consequently, output will end in LF whether input did or not.
use strict;
use warnings;
use 5.18.2;
my $text;
my $comment;
while (<>) {
    # Non-greedy: match until first non-escaped %
    # Without final ([\n]?), pathological last line would not match and an entire last line of comment would be mistaken for text.
    if (m/(^.*?)((?<!\x5c)%.*)([\n]?)/) {
        $text=$1;
        $comment="$2";
    }
    else {
        s/\n//g; # There can be at most one LF, at the end; remove it if it exists.
        $text=$_;
        $comment="";
    }
    # Here, 
    # (1) examine $text for LaTeX-illegal characters; if found, exit with informative error
    # (2) identify LaTeX environments such as \verbatim and \verb, which are to be left alone
    # (3) perform any desired global changes on remaining text
    $text=~s/dog/CAT/g;
    # Add LF back in which we explicitly removed above 
    print "$text$comment\n";
}

0 commentaires