RegEx仅替换LaTeX注释的左侧

时间:2019-05-15 16:51:26

标签: regex perl latex comments tex

我要转换的文件(LaTeX)包含注释,这些注释位于%的右侧。任何未转义的百分号都标记为注释。

我说要使用perl进行正则表达式替换

s/dog/CAT/g

,但仅用于非注释文本。因此,线

  

有一只狗吃了一只老鼠,但有5%的狗吃了那只苹果%的狗?

     

我的狗比你的荣誉学生更聪明

将被转换为

  

一只猫吃了一只老鼠,但是有5%的猫吃了一只苹果%狗?

     

我的CAT比荣誉学生更聪明

当然,这里是如何匹配未转义的百分号:

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

这一定是一个众所周知的问题,但是我没有找到正确的搜索词来找到答案。不能使用单个正则表达式在perl中做到这一点吗?显然,我的替换正则表达式将每个 dog替换为CAT,即使在注释中也是如此。

4 个答案:

答案 0 :(得分:1)

一种方法:提取所有文本(不转义)%,然后在其中进行替换

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

/e修饰符使替换面被视为代码,我们在其中运行了正则表达式。

我们需要首先保存在%中捕获的行的“其余部分”(在$2之后),因为$2将在即将到来的正则表达式中清除。

该正则表达式中的修饰符/r使其返回转换后的字符串,便于形成用作替换的值(通过将其与行的其余部分连接在一起)。另外,在/r下保持原始状态不变,这使我们可以在$1(只读)上使用替换。


上面的[^\\]要求\之前的%以外的其他字符才能开始注释。但是,当它查询一个字符时,如果该行以% 开头并且进一步转义了%,则它使整个正则表达式匹配,这是错误的,并且可能是:在某个时候有评论(使用%),但随后也被评论了。

如果确实有可能,请改用否定的前瞻,这还需要更多

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

请注意,这样做的必要回溯会影响效率。在偶尔的Latex文件中,这应该不是问题,但是如果很多完成,则可能是这样。在这种情况下,请正确解析每一行,从而不需要环视。

使用输入文件data.txt

进行测试
One dog 5\% of dogs % dog
%dog more than 10\% of % dogs
dogs \% and dogs

单线

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

打印

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

答案 1 :(得分:1)

将它分为两​​个任务可能更简单:找到不是注释的字符串部分,然后在该部分进行替换。这是一种解决方法:

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;
}

这使用negative lookbehind来查找第一个未转义的百分号,即使它是字符串的第一个字符也是如此,并且使注释为一半可选,因此如果没有注释,它将仍然替换。但是,它仍然会涉及很多backtracking,因此,如果要考虑性能,则最好采用更广泛的实现。

编辑:这看起来如此复杂的原因是,您尝试执行正则表达式并不是很擅长。您想根据上下文状态在字符串中查找内容。实现此目的的“更好”方法是将字符串解析为标记,这通常通过保留状态和正则表达式的循环来完成(这在这方面很出色);即使只是“非注释字符串”,“注释开始”,“注释字符串”的标记。然后,您可以轻松地仅对非注释字符串进行操作。

这是扩展算法的外观,我尝试将其简化为这种情况所需的解析量,并且可以肯定地将其进一步推广。关键是使用m/\G.../g逐步解析字符串(\G在标量上下文中使用/g修饰符将匹配项锚定到最后一个匹配项的末尾),并依赖于正则表达式引擎选择与字符串中的该点匹配的第一个替换选项。这样,您就可以按顺序遍历字符串,而不会发生回溯,并将状态保持在循环之外。

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";

这是一种不同的令牌化方法,允许您通过跟踪下一个令牌是否被转义来处理转义的反斜杠(如果允许):

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;

Parser::MGC是该概念到对象接口的抽象。

(此外:此方法并不总是比单个回溯正则表达式快,尤其是在解析更简单,行更短的情况下。)

答案 2 :(得分:0)

基于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 

请注意,这允许在非注释文本之后立即添加注释标记;它不需要在%之前加空格。

答案 3 :(得分:0)

#!/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";
}