开发者

Using Unix tools to process text: search and replace all text that are not between some lines

开发者 https://www.devze.com 2023-04-12 07:19 出处:网络
I\'m looking to do some text processing on a bunch of *.org files.I would like to change the following in each file:

I'm looking to do some text processing on a bunch of *.org files. I would like to change the following in each file:

[my description](link)

to

[[link][my description]]

,

`some text`

to

=some text=

,

## some heading

to

** some heading

,

*some italics*

to

/some italics/

, and

**some bold**

to

*some bold*

. Yes, this IS markdown syntax to org-mode syntax. I AM aware of开发者_开发技巧 pandoc. The caveat is that I want the above changes, except when they occur in the following block:

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

Hence, I can't use pandoc. I'd like to process these files according to the above requirements using some kind of unix script: awk, sed, python, perl, bash, etc. Once I have a working script, I can modify it and learn from it.

Thanks for your help!


Perl Solution

This is the result of the simplifying changes I suggested for @jkerian’s script: use the flipflop operator and -p. I’ve also fixed his regexes to use the correct $1 and $2 in the RHS, altered delimiters from s/// to s::: to avoid LTS (“Leaning Toothpick Syndrome”), and added /x to improve readability. There was a logic error in dealing with bold and italics, which I’ve fixed. I added comments showing what the transform should be in each case, corresponding to the original problem description, and aligned the RHS of the transforms to make them easier to read.

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

See how easy that is? Just 6 simple lines of eminently readable code in Perl. It’s easy in Perl because Perl is specifically designed to make writing this sort of filter super-easy, and Python is not. Python has separate design goals.

Although you could certainly rewrite this in Python, it wouldn’t be worth the bother because Python simply isn’t designed for this sort of thing. Python’s missing the -p “make-me-a-filter” flag for an implicit loop and implicit print. Python is missing the implicit accumulator variable. Python is missing built-in regexes. Python is missing the s/// operator. And Python is missing the stateful flipflop operator. All those contribute to making the Perl solution much much much easier to read, write, and maintain than the Python solution.

However, you should not get the idea that this always holds. It doesn’t. In other areas, you can come up with problems that Python comes out ahead in those areas. But not here. It’s because this filter thing is a focused specialty area for Perl, and it isn’t for Python.

The Python solution would consequently be much longer, noisier, and harder to read — and therefore harder to maintain — than this easy Perl version, all because Perl was designed to make easy things easy, and this is one of its target application areas. Try rewriting this in Python and notice how nasty it is. Sure it’s possible, but not worth the hassle, or the maintenance nightmare.

Python Version

#!/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)

Summary

It’s important to use the right tool for the right job. For this task, that tool is Perl, which took only 7 lines. There are only 7 things to do, but don’t try telling Python that. It’s like being back to assembly language with too many interrupt stacks. Python at 72 lines is clearly not cut out for this kind of work, and all the painful complexity and noisey unreadable code shows you just exactly why. Bug rate per line of code is the same no matter the language, so if you have your choice between writing N lines of code or 10*N lines of code, there is no choice.


I think you're looking for something like the following perl script

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

Run it with cat testfile | perl scriptname.pl

For a non-silly version of python. Note: Perl is the right tool for the job, but tchrist's python version is such a bad joke that it had to be fixed.

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="")


Just for grins, here's my version of the python solution:

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='')
0

精彩评论

暂无评论...
验证码 换一张
取 消