今天在微信群里见到一个需求,需要把类似如下文本中,偶数行(即fasta的序列)的左右两端的“-”字符替换成“?”:

text
1>seq-1
2---ACTGACTG---
3>seq-2
4--AC--AC--AC--
5>seq-3
6--ACG---ACG----

当即有群友给出了awk的解决方案:

sh
1cat in.fa \
2  | awk '{if(NR%2==1){print$0}else{gsub("-","?",$0);print$0}}'

这个方案很简洁,且有效。awk对于这种需要指定特定行做操作的需求,确实很方便。

然而,这种方案会把序列中间的“-”也进行替换,这不符合需求,提问者希望保留中间的“-”不替换,而只替换两个末端的“-”。

正则表达式很容易提取左右两个末端的一个或多个“-”(/(^-*|-*$/),然而要指定替换结果为另一个字符组成的同长度字符串,却有些困难了。

于是,只好祭出字符处理的终极武器Perl,让正则替换过程,使用一个代码块(更详细的用法可参见“perldoc perlre”命令列出的详细文档)来进行:

sh
1$ echo -e ">a-a\n--ha-ha---" \
2  | perl -ne 'if($i++%2){$_=~s{^(-*)(.*?)(-*)$}{("?"x(length($1))).$2.("?"x(length($3)))}ex};print'

由于原输入文件来自Windows系统,其行结束符("\r\n")导致了右侧替换失败,于是进一步改进了这段“咒语”:

sh
1$ echo -e ">a-a\n--ha-ha---" \
2  | unix2dos \
3  | perl -ne 'if($i++%2){$_=~s{^(-*)(.*?)(-*)\r?$}{("?"x(length($1))).$2.("?"x(length($3)))}ex};print'

问题解决。

小结:Perl虽然早已不是最好的语言了,但在字符串处理上,却的确是“终极”的,因为现代的各种正则处理,基本都是直接或间接从Perl的各种支持逐步演变过来的。只可惜,Perl这门语言太过古典,很多“咒语”吟唱起来太过拗口冗长,被淘汰恐怕是迟早的事了。或许,未来这种吟唱,可以被机器代替,以另一种更友好的操作界面来进行呢?