使用Unix工具处理文本:搜索并替换不在某些行之间的所有文本

4 投票
3 回答
630 浏览
提问于 2025-04-17 04:04

我想对一堆 *.org 文件进行一些文本处理。我希望在每个文件中做以下更改:

[my description](link)

改成

[[link][my description]]

`some text`

改成

=some text=

## some heading

改成

** some heading

*some italics*

改成

/some italics/

,和

**some bold**

改成

*some bold*

。对的,这些是从 markdown 语法转换成 org-mode 语法。我知道有一个工具叫做 pandoc。但是,我想要的这些更改,除了在下面这个块中出现的时候:

#+BEGIN_EXAMPLE
don't want above changes to take place in this block
...
#+END_EXAMPLE

所以,我不能使用 pandoc。我想用某种 unix 脚本来处理这些文件,比如 awk、sed、python、perl、bash 等等。一旦我有了一个能用的脚本,我可以对它进行修改并从中学习。

谢谢你的帮助!

3 个回答

1

为了好玩,这里是我写的Python解决方案:

from __future__ import print_function
import fileinput, functools, re, sys

# For those desperate to hide tracebacks in one-off scripts
sys.tracebacklimit = 0
# Precompile all our patterns for speed
begin_example = re.compile(r'^#\+BEGIN_EXAMPLE').match
end_example = re.compile(r'^#\+END_EXAMPLE').match
# Use partial to eliminate lookups inside our loop
fixes = [ functools.partial(re.compile(x[0], x[2]).sub, x[1]) for x in
          (r'` ( [^`]* ) `',                      r'=\1=',       re.VERBOSE),
          (r'\[ ( [^]]* ) \] \( ( [^)]* ) \) ',   r'[[\2][\1]]', re.VERBOSE),
          (r'^\#\#',                              r'**',         re.VERBOSE),
          (r'(?!< \* ) \* ( [^*]+ ) \* (?! \*)',  r'/\1/',       re.VERBOSE),
          (r'\*{2} ( [^*]+ ) \*{2}',              r'*\1*',       re.VERBOSE),
          ]

inside = False
for line in fileinput.input():
    if inside:
        if end_example(line):
            inside = False
    else:
        if begin_example(line):
            inside = True
        for fixup in fixes:
            line = fixup(line)
    print(line, end='')
4

我觉得你可能在找类似下面的perl脚本。

while(<>) {
    if /#\+BEGIN_EXAMPLE/ .. /#\+END_EXAMPLE/ {
        print;
        next;
    }
    s/`([^`]*)`/=\1=/g;
    s/\[([^]]*)\]\(([^)]*)\)/[[\2][\1]]/g;
    s/^##/**/;
    s/\*([^\*]+)\*/\/\1\//g;
    s/\*\/([^\/]+)\/\*/*\1*/g;
    print;
}

你可以用 cat testfile | perl scriptname.pl 来运行它。

这是一个不那么搞笑的python版本。注意:虽然perl是做这件事的合适工具,但tchrist的python版本实在太糟糕了,所以必须修正一下。

from __future__ import print_function
import fileinput
import re
import sys

sys.tracebacklimit=0    #For those desperate to hide tracebacks in one-off scripts
example = 0
for line in fileinput.input():
    if example==0 and re.match(r'^#\+BEGIN_EXAMPLE',line):
        example+=1
    elif example>=1:
        if re.match(r'^#\+END_EXAMPLE',line): example-=1
    else:
        line = re. sub (r'` ( [^`]* ) `',                      r'=\1=',       line, 0, re.VERBOSE)
        line = re. sub (r'\[ ( [^]]* ) \] \( ( [^)]* ) \) ',   r'[[\2][\1]]', line, 0, re.VERBOSE)
        line = re. sub (r'^\#\#',                              r'**',         line, 0, re.VERBOSE)
        line = re. sub (r'(?!< \* ) \* ( [^*]+ ) \* (?! \*)',  r'/\1/',       line, 0, re.VERBOSE)
        line = re. sub (r'\*{2} ( [^*]+ ) \*{2}',              r'*\1*',       line, 0, re.VERBOSE)
    print(line, end="")
6

Perl 解决方案

这是我为 @jkerian 的脚本建议的简化修改后的结果:使用 flipflop 操作符和 -p。我还修正了他的正则表达式,确保在右侧使用正确的 $1$2,把分隔符从 s/// 改成 s:::,这样可以避免 LTS(“倾斜牙签综合症”),并添加了 /x 来提高可读性。处理粗体和斜体时有个逻辑错误,我也修正了。我添加了注释,说明在每种情况下应该如何转换,这与原始问题描述相对应,并且调整了转换的右侧,使其更易于阅读。

#!/usr/bin/perl -p
#
# the -p option makes this a pass-through filter
#####################################################

# omit protected region
next if /^#\+BEGIN_EXAMPLE/ .. /^#\+END_EXAMPLE/;

# `some text`                      ⇒   =some text=
s: ` ( [^`]* ) `                       :=$1=:gx;

# [desc](link)                     ⇒   [[link][desc]]
s: \[ ( [^]]* ) \] \( ( [^)]* ) \)     :[[$2][$1]]:gx;

# ^## some heading ⇒ ** some heading
#      NB: can't use /x here or would have to use ugly \#
s:^##:**:;   

# *some italics*                   ⇒   /some italics/
s: (?!< \* ) \* ( [^*]+ ) \* (?! \*)   :/$1/:gx;

# **some bold**                    ⇒   *some bold*
s: \*{2} ( [^*]+ ) \*{2}               :*$1*:gx;

看看这有多简单?仅仅 6 行清晰易读的 Perl 代码。用 Perl 写这个很简单,因为 Perl 专门设计来让写这种过滤器变得超级简单,而 Python 则不是。Python 的设计目标不同。

虽然你当然可以用 Python 重写这个,但这样做并不值得,因为 Python 根本不是为这种事情设计的。Python 缺少 -p 这个“让我变成过滤器”的标志,缺少隐式循环和隐式打印。Python 没有隐式累加变量,也没有内置的正则表达式。Python 还缺少 s/// 操作符,以及有状态的 flipflop 操作符。这些都使得 Perl 的解决方案比 Python 的解决方案更容易阅读、编写和维护。

不过,你不应该认为这种情况总是成立。并不是。在其他领域,你可能会发现 Python 更占优势。但在这里不是。因为这个过滤器的事情是 Perl 的一个专门领域,而 Python 并不是。

因此,Python 的解决方案会更长、更复杂、更难读——因此也更难维护——而这一切都是因为 Perl 被设计成让简单的事情变得简单,而这正是它的目标应用领域。试着用 Python 重写这个,看看有多麻烦。虽然可以做到,但不值得费这个劲,或者说是维护的噩梦。

Python 版本

#!/usr/bin/env python3.2

from __future__ import print_function

import sys
import re

if (sys.version_info[0] == 2):
    sys.stderr.write("%s: legacy Python detected! Please upgrade to v3+\n"
                   % sys.argv[0] )
    ##sys.exit(2)

if len(sys.argv) == 1:
    sys.argv.append("/dev/stdin")

flip_rx = re.compile(r'^#\+BEGIN_EXAMPLE')
flop_rx = re.compile(r'^#\+END_EXAMPLE')

#EG# `some text`  -->   =some text=
lhs_backticks = re.compile(r'` ( [^`]* ) `', re.VERBOSE)
rhs_backticks =            r'=\1='

#EG# [desc](link)  -->  [[link][desc]]
lhs_desclink  = re.compile(r' \[ ( [^]]* ) \] \( ( [^)]* ) \) ', re.VERBOSE)
rhs_desclink  =            r'[[\2][\1]]'

#EG# ^## some heading  -->  ** some heading
lhs_header    = re.compile(r'^##')
rhs_header    =            r'**'

#EG# *some italics*  -->  /some italics/
lhs_italics   = re.compile(r' (?!< \* ) \* ( [^*]+ ) \* (?! \*)  ', re.VERBOSE)
rhs_italics   =            r'/\1/'

## **some bold**  -->  *some bold*
lhs_bold      = re.compile(r'\*{2} ( [^*]+ ) \*{2}', re.VERBOSE)
rhs_bold      =            r'*\1*'

errcnt = 0

flipflop = "flip"

for filename in sys.argv[1:]:
    try:
        filehandle = open(filename, "r")
    except IOError as oops:
        errcnt = errcnt + 1
        sys.stderr.write("%s: can't open '%s' for reading: %s\n"
                      % ( sys.argv[0],    filename,        oops) )
    else:
        try:
            for line in filehandle:

                new_flipflop = None

                if flipflop == "flip":
                    if flip_rx.search(line):
                        new_flipflop = "flop"
                elif flipflop == "flop":
                    if flop_rx.search(line):
                        new_flipflop = "flip"
                else:
                    raise FlipFlop_SNAFU

                if flipflop != "flop":
                    line = lhs_backticks . sub ( rhs_backticks, line)
                    line = lhs_desclink  . sub ( rhs_desclink,  line)
                    line = lhs_header    . sub ( rhs_header,    line)
                    line = lhs_italics   . sub ( rhs_italics,   line)
                    line = lhs_bold      . sub ( rhs_bold,      line)                        
                print(line, end="")

                if new_flipflop != None:
                    flipflop = new_flipflop

        except IOError as oops:
            errcnt = errcnt + 1
            sys.stderr.write("%s: can't read '%s': %s\n"
              % ( sys.argv[0],    filename,        oops) )
        finally:
            try:
                filehandle.close()
            except IOError as oops:
                errcnt = errcnt + 1
                sys.stderr.write("%s: can't close '%s': %s\n"
                  % ( sys.argv[0],    filename,        oops) )

if errcnt == 0:
    sys.exit(0)
else:
    sys.exit(1)

总结

使用合适的工具来完成合适的工作是很重要的。对于这个任务,合适的工具是 Perl,只用了 7 行代码。只需做 7 件事,但别试图告诉 Python 这一点。这就像回到了汇编语言,堆栈中有太多中断。Python 的 72 行代码显然不适合这种工作,而所有那些复杂且难以阅读的代码正好说明了这一点。每行代码的错误率在任何语言中都是一样的,所以如果你可以选择写 N 行代码还是 10*N 行代码,那就没有选择的余地。

撰写回答