如何使用sed/awk查找/替换并递增匹配的数字?

53

直截了当地说,我想知道如何使用grep/find/sed/awk来匹配以数字结尾的特定字符串并将该数字增加1。目前我最接近的方法是在末尾连接一个1(这已经足够好用),因为主要目的只是改变这个值。以下是我目前正在做的事情:

find . -type f | xargs sed -i 's/\(\?cache_version\=[0-9]\+\)/\11/g'

因为我无法找出如何使数字递增,所以我将整个内容捕获,然后只是追加了一个“1”。 之前我的代码像这样:

find . -type f | xargs sed -i 's/\?cache_version\=\([0-9]\+\)/?cache_version=\11/g'

所以至少我了解如何获取我所需的内容。

不是解释这是什么,而是解释我想要它执行的操作。它应该基于当前目录(不重要,可以是任何目录,所以稍后我会进行配置),递归地在任何文件中查找与数字 "?cache_version=" 匹配的文本,然后增加该数字并替换文件中的原数字。

目前我拥有的东西可以正常工作,只是无法在找到的数字末尾递增它。最好能够增加而不是附加 "1",这样未来的值就不会为 "11"、"111"、"1111"、"11111" 等。

我已经阅读了数十篇文章/解释,经常建议使用 awk,但我却不能将它们混合使用。我接近使用 awk 的方法是下面这个,它实际上不会替换任何内容:

grep -Pro '(?<=\?cache_version=)[0-9]+' . | awk -F: '{ print "match is", $2+1 }'

我想知道是否有一种方法可以在结尾处使用sed并传递原文件名称,以便sed可以获取文件名和增加的数字(来自awk),或者它所需要的任何内容,与xargs类似。

从技术上讲,这个数字并不重要;这个替换的主要目的是确保那里有一个新的数字,100%肯定不同于上一个。所以当我写这个问题时,我意识到我可能也可以使用系统时间-自纪元以来的秒数(AJAX经常使用的技术),以消除后续“相同”的请求的缓存。我最终得到了以下代码,看起来很完美:

CXREPLACETIME=`date +%s`; find . -type f | xargs sed -i "s/\(\?cache_version\=\)[0-9]\+/\1$CXREPLACETIME/g"

(我先存储这个值,以便所有文件得到相同的值,以防它由于某种原因跨越了多个秒)

但我仍然希望知道原始问题,即如何递增匹配的数字。我猜想一个简单的解决方案是将其变成Bash脚本,但是我认为还有一种比逐个递归文件并检查其内容是否匹配再替换更简单的方式,因为它只是递增匹配的数字...没有太多其他的逻辑。 我只是不想写入任何其他文件或类似的东西-它应该就地完成,就像sed使用"i"选项一样。

5个回答

72

我觉得你不难找到文件。因此,我直接来点,进行+1的计算。如果你有gnu sed,可以这样做:

sed -r 's/(.*)(\?cache_version=)([0-9]+)(.*)/echo "\1\2$((\3+1))\4"/ge' file

让我们举一个例子:

kent$  cat test 
ello
barbaz?cache_version=3fooooo
bye

kent$  sed -r 's/(.*)(\?cache_version=)([0-9]+)(.*)/echo "\1\2$((\3+1))\4"/ge' test     
ello                                                                             
barbaz?cache_version=4fooooo
bye

如果你愿意的话,可以添加 -i 选项。

编辑

/e 允许你将匹配部分传递给外部命令,并用执行结果进行替换。只有 Gnu sed 可用。

看这个例子: 使用外部命令/工具 echo, bc

kent$  echo "result:3*3"|sed -r 's/(result:)(.*)/echo \1$(echo "\2"\|bc)/ge'       

输出:

result:9

你可以使用其他强大的外部命令,比如 cut、sed(再次)、awk 等等...


抱歉,我也意识到了 echo/e 标志。您能再详细解释一下吗? - Ian
如果文本文件中的文本/行类似于<script type="text/javascript" src="asdf.js?cache_version=2"></script>,那么我会收到关于“未预期的标记“<”附近的语法错误”的错误。然后,它还会剥离"字符(可能更多)。上面有两个注释,其中包含我的当前命令。这有意义吗? - Ian
2
啊,我明白了。我发誓我已经做过了。好的,这解决了“<”字符的问题。现在唯一的问题是当写回文件时,“"”被剥离了。你知道是什么原因吗?也许只是一个“-i”的问题,但很奇怪。脚本运行之前有“"”,但运行后就消失了。 - Ian
1
哦,抱歉,没有注意到引号。那么脏而快速的解决方法是:...|sed 's/"/\\"/g'|sed -r 's/(.*)(\?cache_version=)([0-9]+)(.*)/echo "\1\2$((\3+1))\4"/ge' - Kent
第一个例子对我不起作用。我在Cygwin上使用sed(GNU sed)4.4,但我不知道为什么会有问题。使用-i将数字更改为24而不是4。没有-i,我不确定它会做什么,因为输出很奇怪,只显示最后一行。也许这可以归咎于我的终端。但在Linux服务器(GNU sed版本4.2.1)上,它按预期工作。第二个例子在两个系统上都有效。 - piojo
显示剩余8条评论

10

sed版本:

这个版本不依赖于其他命令或环境变量。它使用明确的进位方式。我使用@符号作为进位符,但如果您喜欢,可以使用另一个名称。请使用输入文件中不存在的内容。 首先找到SEARCHSTRING<number>并在其后添加@。 它重复增加具有待处理进位的数字(即,在其后具有进位符:[0-9]@)。 如果9被增加,则此增加本身会产生一个进位,并且该过程将重复,直到没有更多的待处理进位。 最后,曾经产生但尚未添加到数字中的进位将被替换为1。

sed "s/SEARCHSTRING[0-9]*[0-9]/&@/g;:a {s/0@/1/g;s/1@/2/g;s/2@/3/g;s/3@/4/g;s/4@/5/g;s/5@/6/g;s/6@/7/g;s/7@/8/g;s/8@/9/g;s/9@/@0/g;t a};s/@/1/g" numbers.txt

2
好的解决方案。为了不替换@符号,似乎可以将@替换为另一个不太常见的符号,比如£:sed "s/cache_version=[0-9]*[0-9]/&£/g;:a {s/0£/1/g;s/1£/2/g;s/2£/3/g;s/3£/4/g;s/4£/5/g;s/5£/6/g;s/6£/7/g;s/7£/8/g;s/8£/9/g;s/9£/£0/g;t a};s/£/1/g" $1 - Robin Manoli
注意,在 MacOS 上,标签的名称以换行符结束,因此您必须将命令分成两行才能使其正常工作。 - Matt

9

这个 perl 命令将搜索当前目录中的所有文件(不遍历它,你需要 File::Find 模块或类似的更复杂任务),并将匹配 cache_version= 的行号加一。它使用了正则表达式的 /e 标记来评估替换部分。

perl -i.bak -lpe 'BEGIN { sub inc { my ($num) = @_; ++$num } } s/(cache_version=)(\d+)/$1 . (inc($2))/eg' *

我用当前目录中的file进行了测试,测试数据如下:

hello
cache_version=3
bye

它备份原始文件(ls -1):

file
file.bak

现在使用 file 命令:

hello
cache_version=4
bye

我希望这对你寻找相关内容有所帮助。


更新:使用File::Find遍历目录。它可以接受*作为参数,但会将其与使用File::Find发现的文件一起丢弃。搜索开始的目录是脚本执行的当前目录。其在代码行find( \&wanted, "." )中硬编码。

perl -MFile::Find -i.bak -lpe '

    BEGIN { 
        sub inc { 
            my ($num) = @_; 
            ++$num 
        }

        sub wanted {
            if ( -f && ! -l ) {  
                push @ARGV, $File::Find::name;
            }
        }

        @ARGV = ();
        find( \&wanted, "." );
    }

    s/(cache_version=)(\d+)/$1 . (inc($2))/eg

' *

File::Find 是一个核心模块,供您参考。 - squiguy
这绝对是有用的!我本来不是在找Perl,但是嘿,如果它能工作,就不能抱怨了。你能否提供如何使用Find::File以便它可以递归搜索目录的说明?如果我不想备份,我应该只删除-i.bak吗?感谢您的帮助! - Ian
@Ian:我已经更新了答案,加入了一个脚本来遍历目录,搜索所有常规文件中的字符串并对其进行修改,同时创建备份文件。使用-i选项不带扩展名(像perl -MFile :: Find -i -lpe ...)可以直接在原文件上进行修改,但这可能有一定的风险。 - Birei
我更喜欢 Perl 而不是 Gnu sed,因为它在 Mac 上是预安装的。 - Frank Harper
inc 函数有什么用?为什么不直接使用 (1+$2) - qbolec

4

虽然我有点生疏,但以下是使用sed的开始:

这很丑,但是在使用sed时,它是一个好的起点。
orig="something1" ;
text=`echo $orig | sed "s/\([^0-9]*\)\([0-9]*\)/\1/"` ;
num=`echo $orig | sed "s/\([^0-9]*\)\([0-9]*\)/\2/"` ;
echo $text$(($num + 1))

原始文件名($orig)为“something1”,sed将文本和数字部分拆分为$text$num,然后在最后一部分中将它们组合起来并增加一个数字,结果为something2

这只是一个开始,因为它没有考虑文件名中包含数字或名称末尾没有数字的情况,但希望对您使用sed的原始目标有所帮助。

我相信这可以通过使用缓冲区在sed内部简化(sed可以递归操作),但是我对此方面实在很生疏。


嗯...重新阅读你的问题,我不认为我已经回答了它,但希望其中至少有一些有用的东西。我确实相信我已经做过类似于你正在做的事情,完全使用sed,但不幸的是现在没有那个解决方案。 - David Ravetti
你说得没错,这不完全是你要找的,但这是正确的想法,而且肯定在正确的轨道上。希望没关系,但我把行分开了,这样我可以更好地阅读它。我有几个问题/评论,所以可能会跨越多个评论。真的很快-为什么要转义\?我真的不想在文件名上进行搜索/替换,而是在文件内容上进行。所以就像我在原始帖子中说的那样,如果有一种方法可以制作一个使用您拥有的逻辑的bash脚本,但是 - Ian
修改文件内容并保存,这真的很有帮助。如果您有任何想法如何将它们组合起来实现两者兼顾。您最终需要的代码行就在sed的替换部分内。但是像您所拥有的那样将这些东西分成单独的语句有点违背我的需求。我考虑使用find,然后遍历结果,然后使用您的逻辑和最终的sed -i来真正替换值...但我不确定它是否可以这样工作。你知道我的意思吗?感谢您的帮助! - Ian
反引号的转义仅用于帖子内部的显示,因为该字符也用于在此处定义代码块 - 实际上它没有被转义(我可能意外地进行了双重转义或类似操作)。我相信您提出的内容可以通过sed -i实现,使用sed的一些更高级功能。但是,我不确定bash数学函数是否完全在sed中运行,因此您可能需要更复杂的“用[1-9]替换[0-8],用[10]替换[9]”之类的替换。我看到了许多有关“sed中的数学”的结果,但它们似乎大多说,“请尝试使用awk或perl”。 - David Ravetti
啊,好的,我简直不敢相信我没有想到\``是用于代码的,可能是问题所在。我只是不确定是否有bash或其他特殊原因。你不会碰巧知道关于awk的任何信息吗?这里的另一个答案使用sed非常接近,这里的perl答案也很好。似乎只有用awk`尝试一下才合适 :) - Ian
我对awk的经验非常有限,主要是用于从日志文件中获取字段,然后通过sed进行操作。我很少使用它,以至于每次都必须搜索或查找书籍来记住正确的格式。抱歉。 - David Ravetti

1
perl -pi -e 's/(\?cache_version=)(\d+)/$1.($2+1)/ge' FILE [FILE...]

或者获取完整解决方案:

find . -type f | xargs perl -pi -e 's/(\?cache_version=)(\d+)/$1.($2+1)/ge'

Perl替换运算符

  • /e修饰符将替换操作作为Perl语句进行评估,并使用其返回值作为替换文本。
  • .运算符在Perl中连接字符串。括号确保算术运算$2+1优先于连接操作。
  • /g修饰符将替换应用于行内所有匹配的字符串。

Perl选项

  • -p确保Perl对每个文件的每一行执行命令。
  • -i确保每个文件都会被原地编辑。
  • -e指定要执行的Perl命令(在这种情况下,是替换操作)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接