使用Unix工具处理文本:搜索并替换不在某些行之间的所有文本
我想对一堆 *.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 个回答
为了好玩,这里是我写的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='')
我觉得你可能在找类似下面的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="")
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 行代码,那就没有选择的余地。