• shell之sed命令全解
    时间:2009-06-17   作者:佚名   出处:互联网

    请注意新的 '-n' 选项,该选项告诉 sed 除非明确要求打印模式空间,否则不这样做。您还会注意到,我们用 'p' 命令替换了 'd' 命令,如您所猜想的那样,这明确要求 sed 打印模式空间。就这样,将只打印匹配部分。

    有关地址的更多内容
    目前为止,我们已经看到了行地址、行范围地址和 regexp 地址。但是,还有更多的可能。我们可以指定两个用逗号分开的规则表达式,sed 将与所有从匹配第一个规则表达式的第一行开始,到匹配第二个规则表达式的行结束(包括该行)的所有行匹配。例如,以下命令将打印从包含 "BEGIN" 的行开始,并且以包含 "END" 的行结束的文本块:

    $ sed -n -e '/BEGIN/,/END/p' /my/test/file | more

    如果没发现 "BEGIN",那么将不打印数据。如果发现了 "BEGIN",但是在这之后的所有行中都没发现 "END",那么将打印所有后续行。发生这种情况是因为 sed 面向流的特性 -- 它不知道是否会出现 "END"。

    C 源代码示例
    如果只要打印 C 源文件中的 main() 函数,可输入:

    $ sed -n -e '/main[[:space:]]*(/,/^}/p' sourcefile.c | more

    该命令有两个规则表达式 '/main[[:space:]]*(/' 和 '/^}/',以及一个命令 'p'。第一个规则表达式将与后面依次跟有任意数量的空格或制表键以及开始圆括号的字符串 "main" 匹配。这应该与一般 ANSI C main() 声明的开始匹配。

    在这个特别的规则表达式中,出现了 '[[:space:]]' 字符类。这只是一个特殊的关键字,它告诉 sed 与 TAB 或空格匹配。如果愿意的话,可以不输入 '[[:space:]]',而输入 '[',然后是空格字母,然后是 -V,然后再输入制表键字母和 ']' -- Control-V 告诉 bash 要插入“真正”的制表键,而不是执行命令扩展。使用 '[[:space:]]' 命令类(特别是在脚本中)会更清楚。

    好,现在看一下第二个 regexp。'/^}' 将与任何出现在新行行首的 '}' 字符匹配。如果代码的格式很好,那么这将与 main() 函数的结束花括号匹配。如果格式不好,则不会正确匹配 -- 这是执行模式匹配任务的一件棘手之事。

    因为是处于 '-n' 安静方式,所以 'p' 命令还是完成其惯有任务,即明确告诉 sed 打印该行。试着对 C 源文件运行该命令 -- 它应该输出整个 main() { } 块,包括开始的 "main()" 和结束的 '}'。

    下一篇
    既然已经触及了基本知识,我们将在后两篇文章中加快步伐。如果想看一些更丰富的 sed 资料,请耐心一些 -- 马上就有!同时,您可能想查看下列 sed 和规则表达式资源。

    sed 是十分强大和小巧的文本流编辑器。在本文章系列的第二篇中,Daniel Robbins 为您演示如何使用 sed 来执行字符串替换、创建更大的 sed 脚本以及如何使用 sed 的附加、插入和更改行命令。
    sed 是很有用(但常被遗忘)的 UNIX 流编辑器。在以批处理方式编辑文件或以有效方式创建 shell 脚本来修改现有文件方面,它是十分理想的工具。本文是前一篇介绍 sed 文章的续篇。

    替换!
    让我们看一下 sed 最有用的命令之一,替换命令。使用该命令,可以将特定字符串或匹配的规则表达式用另一个字符串替换。下面是该命令最基本用法的示例:

    $ sed -e 's/foo/bar/' myfile.txt
    上面的命令将 myfile.txt 中每行第一次出现的 'foo'(如果有的话)用字符串 'bar' 替换,然后将该文件内容输出到标准输出。请注意,我说的是每行第一次出现,尽管这通常不是您想要的。在进行字符串替换时,通常想执行全局替换。也就是说,要替换每行中的所有出现,如下所示:

    $ sed -e 's/foo/bar/g' myfile.txt
    在最后一个斜杠之后附加的 'g' 选项告诉 sed 执行全局替换。

    关于 's///' 替换命令,还有其它几件要了解的事。首先,它是一个命令,并且只是一个命令,在所有上例中都没有指定地址。这意味着,'s///' 还可以与地址一起使用来控制要将命令应用到哪些行,如下所示:

    $ sed -e '1,10s/enchantment/entrapment/g' myfile2.txt
    上例将导致用短语 'entrapment' 替换所有出现的短语 'enchantment',但是只在第一到第十行(包括这两行)上这样做。

    $ sed -e '/^$/,/^END/s/hills/mountains/g' myfile3.txt
    该例将用 'mountains' 替换 'hills',但是,只从空行开始,到以三个字符 'END' 开始的行结束(包括这两行)的文本块上这样做。

    关于 's///' 命令的另一个妙处是 '/' 分隔符有许多替换选项。如果正在执行字符串替换,并且规则表达式或替换字符串中有许多斜杠,则可以通过在 's' 之后指定一个不同的字符来更改分隔符。例如,下例将把所有出现的 /usr/local 替换成 /usr:

    $ sed -e 's:/usr/local:/usr:g' mylist.txt
    在该例中,使用冒号作为分隔符。如果需要在规则表达式中指定分隔符字符,可以在它前面加入反斜杠。

    规则表达式混乱
    目前为止,我们只执行了简单的字符串替换。虽然这很方便,但是我们还可以匹配规则表达式。例如,以下 sed 命令将匹配从 '<' 开始、到 '>' 结束、并且在其中包含任意数量字符的短语。下例将删除该短语(用空字符串替换):

    $ sed -e 's/<.*>//g' myfile.html
    这是要从文件除去 HTML 标记的第一个很好的 sed 脚本尝试,但是由于规则表达式的特有规则,它不会很好地工作。原因何在?当 sed 试图在行中匹配规则表达式时,它要在行中查找最长的匹配。在我的前一篇 sed 文章中,这不成问题,因为我们使用的是 'd' 和 'p' 命令,这些命令总要删除或打印整行。但是,在使用 's///' 命令时,确实有很大不同,因为规则表达式匹配的整个部分将被目标字符串替换,或者,在本例中,被删除。这意味着,上例将把下行:

    <b>This</b> is what <b>I</b> meant.
    变成:

    meant.
    我们要的不是这个,而是:

    This is what I meant.
    幸运的是,有一种简便方法来纠正该问题。我们不输入“'<' 字符后面跟有一些字符并以 '>' 字符结束”的规则表达式,而只需输入一个“'<' 字符后面跟有任意数量非 '>' 字符并以 '>' 字符结束”的规则表达式。这将与最短、而不是最长的可能性匹配。新命令如下:

    $ sed -e 's/<[^>]*>//g' myfile.html
    在上例中,'[^>]' 指定“非 '>'”字符,其后的 '*' 完成该表达式以表示“零或多个非 '>' 字符”。对几个 html 文件测试该命令,将它们管道输出到 "more",然后仔细查看其结果。

    更多字符匹配
    '[ ]' 规则表达式语法还有一些附加选项。要指定字符范围,只要字符不在第一个或最后一个位置,就可以使用 '-',如下所示:

    '[a-x]*'
    这将匹配零或多个全部为 'a'、'b'、'c'...'v'、'w'、'x' 的字符。另外,可以使用 '[:space:]' 字符类来匹配空格。以下是可用字符类的相当完整的列表:

    字符类 描述
    [:alnum:] 字母数字 [a-z A-Z 0-9]
    [:alpha:] 字母 [a-z A-Z]
    [:blank:] 空格或制表键
    [:cntrl:] 任何控制字符
    [:digit:] 数字 [0-9]
    [:graph:] 任何可视字符(无空格)
    [:lower:] 小写 [a-z]
    [:print:] 非控制字符
    [:punct:] 标点字符
    [:space:] 空格
    [:upper:] 大写 [A-Z]
    [:xdigit:] 十六进制数字 [0-9 a-f A-F]

    尽可能使用字符类是很有利的,因为它们可以更好地适应非英语 locale(包括某些必需的重音字符等等).

    高级替换功能
    我们已经看到如何执行简单甚至有些复杂的直接替换,但是 sed 还可以做更多的事。实际上可以引用匹配规则表达式的部分或全部,并使用这些部分来构造替换字符串。作为示例,假设您正在回复一条消息。下例将在每一行前面加上短语 "ralph said: ":

    $ sed -e 's/.*/ralph said: &/' origmsg.txt
    输出如下:

    ralph said: Hiya Jim, ralph said: ralph said:
    I sure like this sed stuff! ralph said:
    该例的替换字符串中使用了 '&' 字符,该字符告诉 sed 插入整个匹配的规则表达式。因此,可以将与 '.*' 匹配的任何内容(行中的零或多个字符的最大组或整行)插入到替换字符串中的任何位置,甚至多次插入。这非常好,但 sed 甚至更强大。

    那些极好的带反斜杠的圆括号
    's///' 命令甚至比 '&' 更好,它允许我们在规则表达式中定义区域,然后可以在替换字符串中引用这些特定区域。作为示例,假设有一个包含以下文本的文件:

    foo bar oni eeny meeny miny larry curly moe jimmy the weasel
    现在假设要编写一个 sed 脚本,该脚本将把 "eeny meeny miny" 替换成 "Victor eeny-meeny Von miny" 等等。要这样做,首先要编写一个由空格分隔并与三个字符串匹配的规则表达式。

    '.* .* .*'
    现在,将在其中每个感兴趣的区域两边插入带反斜杠的圆括号来定义区域:

    '(.*) (.*) (.*)'
    除了要定义三个可在替换字符串中引用的逻辑区域以外,该规则表达式的工作原理将与第一个规则表达式相同。下面是最终脚本:

    $ sed -e 's/(.*) (.*) (.*)/Victor 1-2 Von 3/' myfile.txt
    如您所见,通过输入 'x'(其中,x 是从 1 开始的区域号)来引用每个由圆括号定界的区域。输入如下:

    Victor foo-bar Von oni Victor eeny-meeny Von miny Victor larry-curly Von moe Victor jimmy-the Von weasel
    随着对 sed 越来越熟悉,您可以花最小力气来进行相当强大的文本处理。您可能想如何使用熟悉的脚本语言来处理这种问题 -- 能用一行代码轻易实现这样的解决方案吗?

    组合使用
    在开始创建更复杂的 sed 脚本时,需要有输入多个命令的能力。有几种方法这样做。首先,可以在命令之间使用分号。例如,以下命令系列使用 '=' 命令和 'p' 命令,'=' 命令告诉 sed 打印行号,'p' 命令明确告诉 sed 打印该行(因为处于 '-n' 模式)。

    $ sed -n -e '=;p' myfile.txt
    无论什么时候指定了两个或更多命令,都按顺序将每个命令应用到文件的每一行。在上例中,首先将 '=' 命令应用到第 1 行,然后应用 'p' 命令。接着,sed 继续处理第 2 行,并重复该过程。虽然分号很方便,但是在某些场合下,它不能正常工作。另一种替换方法是使用两个 -e 选项来指定两个不同的命令:

    $ sed -n -e '=' -e 'p' myfile.txt
    然而,在使用更为复杂的附加和插入命令时,甚至多个 '-e' 选项也不能帮我们的忙。对于复杂的多行脚本,最好的方法是将命令放入一个单独的文件中。然后,用 -f 选项引用该脚本文件:

    $ sed -n -f mycommands.sed myfile.txt
    这种方法虽然可能不太方便,但总是管用。

    一个地址的多个命令
    有时,可能要指定应用到一个地址的多个命令。这在执行许多 's///' 以变换源文件中的字和语法时特别方便。要对一个地址执行多个命令,可在文件中输入 sed 命令,然后使用 '{ }' 字符将这些命令分组,如下所示:

    1,20{ s/[Ll]inux/GNU/Linux/g s/samba/Samba/g s/posix/POSIX/g }
    上例将把三个替换命令应用到第 1 行到第 20 行(包括这两行)。还可以使用规则表达式地址或者二者的组合:

    1,/^END/{ s/[Ll]inux/GNU/Linux/g s/samba/Samba/g s/posix/POSIX/g p }
    该例将把 '{ }' 之间的所有命令应用到从第 1 行开始,到以字母 "END" 开始的行结束(如果在源文件中没发现 "END",则到文件结束)的所有行。

    附加、插入和更改行
    既然在单独的文件中编写 sed 脚本,我们可以利用附加、插入和更改行命令。这些命令将在当前行之后插入一行,在当前行之前插入一行,或者替换模式空间中的当前行。它们也可以用来将多行插入到输出。插入行命令用法如下:

    i This line will be inserted before each line
    如果不为该命令指定地址,那么它将应用到每一行,并产生如下的输出:

    This line will be inserted before each line line 1 here
    This line will be inserted before each line line 2 here
    This line will be inserted before each line line 3 here
    This line will be inserted before each line line 4 here
    如果要在当前行之前插入多行,可以通过在前一行之后附加一个反斜杠来添加附加行,如下所示:

    i insert this line and this one and this one and, uh, this one too.
    附加命令的用法与之类似,但是它将把一行或多行插入到模式空间中的当前行之后。其用法如下:

    a insert this line after each line. Thanks! :)
    另一方面,“更改行”命令将实际替换模式空间中的当前行,其用法如下:

    c You're history, original line! Muhahaha!
    因为附加、插入和更改行命令需要在多行输入,所以将把它们输入到一个文本 sed 脚本中,然后通过使用 '-f' 选项告诉 sed 执行它们。使用其它方法将命令传递给 sed 会出现问题。

    下一篇
    在下一篇、也是本 sed 系列的最后一篇文章中,我将为您演示许多使用 sed 来完成不同类型任务的极佳实例。我将不仅为您显示脚本做些什么,还显示为什么那样做。完成之后,您将掌握更多有关如何在不同项目中使用 sed 的极佳知识。到时候见!


    在这篇 sed 系列的总结性文章中,Daniel Robbins 带您体验 sed 的真正力量。在介绍完几个重要的 sed 脚本之后,他将通过将一个 Quicken .QIF 文件转换成可读文本格式来演示一些基本 sed 脚本的编写。该转换脚本不仅实用,而且还是展现 sed 脚本编写能力的极佳示例。

    强健的 sed
    在第二篇 sed 文章中,我提供了一些示例来演示 sed 的工作原理,但是它们当中很少有示例能实际做特别有用的事。在这篇 sed 系列的最后文章中,我要改变那种方式,并使用 sed 来做实际的事。我将为您显示几个示例,它们不仅演示 sed 的能力,而且还做一些真正巧妙(和方便)的事。例如,在本文的后半部,将为您演示如何设计一个 sed 脚本来将 .QIF 文件从 Intuit 的 Quicken 金融程序转换成具有良好格式的文本文件。在那样做之前,我们将看一下不怎么复杂但却很有用的 sed 脚本。

    文本转换
    第一个实际脚本将 UNIX 风格的文本转换成 DOS/Windows 格式。您可能知道,基于 DOS/Windows 的文本文件在每一行末尾有一个 CR(回车)和 LF(换行),而 UNIX 文本只有一个换行。有时可能需要将某些 UNIX 文本移至 Windows 系统,该脚本将为您执行必需的格式转换。

    $ sed -e 's/$/ /' myunix.txt > mydos.txt

    在该脚本中,'$' 规则表达式将与行的末尾匹配,而 ' ' 告诉 sed 在其之前插入一个回车。在换行之前插入回车,立即,每一行就以 CR/LF 结束。请注意,仅当使用 GNU sed 3.02.80 或以后的版本时,才会用 CR 替换 ' '。如果还没有安装 GNU sed 3.02.80,请在我的第一篇 sed 文章中查看如何这样做的说明。

    我已记不清有多少次在下载一些示例脚本或 C 代码之后,却发现它是 DOS/Windows 格式。虽然很多程序不在乎 DOS/Windows 格式的 CR/LF 文本文件,但是有几个程序却在乎 -- 最著名的是 bash,只要一遇到回车,它就会出问题。以下 sed 调用将把 DOS/Windows 格式的文本转换成可信赖的 UNIX 格式:

    $ sed -e 's/.$//' mydos.txt > myunix.txt

    该脚本的工作原理很简单:替代规则表达式与一行的最末字符匹配,而该字符恰好就是回车。我们用空字符替换它,从而将其从输出中彻底删除。如果使用该脚本并注意到已经删除了输出中每行的最末字符,那么,您就指定了已经是 UNIX 格式的文本文件。也就没必要那样做了!

    反转行
    下面是另一个方便的小脚本。与大多数 Linux 发行版中包括的 "tac" 命令一样,该脚本将反转文件中行的次序。"tac" 这个名称可能会给人以误导,因为 "tac" 不反转行中字符的位置(左和右),而是反转文件中行的位置(上和下)。用 "tac" 处理以下文件:

    foo bar oni

    ....将产生以下输出:

    oni bar foo

    可以用以下 sed 脚本达到相同目的:

    $ sed -e '1!G;h;$!d' forward.txt > backward.txt

    如果登录到恰巧没有 "tac" 命令的 FreeBSD 系统,将发现该 sed 脚本很有用。虽然方便,但最好还是知道该脚本为什么那样做。让我们对它进行讨论。

    反转解释
    首先,该脚本包含三个由分号隔开的单独 sed 命令:'1!G'、'h' 和 '$!d'。现在,需要好好理解用于第一个和第三个命令的地址。如果第一个命令是 '1G',则 'G' 命令将只应用第一行。然而,还有一个 '!' 字符 -- 该 '!' 字符忽略该地址,即,'G' 命令将应用到除第一行之外的所有行。'$!d' 命令与之类似。如果命令是 '$d',则将只把 'd' 命令应用到文件中的最后一行('$' 地址是指定最后一行的简单方式)。然而,有了 '!' 之后,'$!d' 将把 'd' 命令应用到除最后一行之外的所有行。现在,我们所要理解的是这些命令本身做什么。

    当对上面的文本文件执行反转脚本时,首先执行的命令是 'h'。该命令告诉 sed 将模式空间(保存正在处理的当前行的缓冲区)的内容复制到保留空间(临时缓冲区)。然后,执行 'd' 命令,该命令从模式空间中删除 "foo",以便在对这一行执行完所有命令之后不打印它。

    现在,第二行。在将 "bar" 读入模式空间之后,执行 'G' 命令,该命令将保留空间的内容 ("foo ") 附加到模式空间 ("bar "),使模式空间的内容为 "bar foo "。'h' 命令将该内容放回保留空间保护起来,然后,'d' 从模式空间删除该行,以便不打印它。

    对于最后的 "oni" 行,除了不删除模式空间的内容(由于 'd' 之前的 '$!')以及将模式空间的内容(三行)打印到标准输出之外,重复同样的步骤。

    现在,要用 sed 执行一些强大的数据转换。

    sed QIF 魔法
    过去几个星期,我一直想买一份 Quicken 来结算我的银行帐户。Quicken 是一个非常好的金融程序,当然会成功地完成这项工作。但是,经过考虑之后,我觉得自己可以轻易编写某个软件来结算我的支票簿。我想,毕竟,我是个软件开发人员!

    我开发了一个很好的小型支票簿结算程序(使用 awk),它通过分析包含我的所有交易的文本文件的语法来计算余额。略微调整之后,我将其改进,以便可以象 Quicken 那样跟踪不同的贷款和借款类别。但是,我还要添加一个特性。最近,我将帐户转移到一家有联机 Web 帐户界面的银行。有一天,我注意到,这家银行的 Web 站点允许以 Quicken 的 .QIF 格式下载我的帐户信息。我马上觉得,如果可以将该信息转换成文本格式,那就太棒了。

    两种格式的故事
    在查看 QIF 格式之前,先看一下我的 checkbook.txt 格式:

    28 Aug 2000 food - - Y Supermarket 30.94 25 Aug 2000 watr - 103 Y Check 103 52.86

    在我的文件中,所有字段都由一个或多个制表符分开,每个交易占据一行。日期之后的下一个字段列出支出类型(如果是收入项,则为 "-")。第三个字段列出收入类型(如果是支出项,则为 "-")。然后,是一个支票号字段(如果为空,则还是 "-"),一个交易完成字段("Y" 或 "N"),一个注释和一个美元金额字段。现在,让我们看一下 QIF 格式。当用文本查看器查看下载的 QIF 文件时,它看起来如下:

    !Type:Bank D08/28/2000 T-8.15 N PCHECKCARD SUPERMARKET ^ D08/28/2000 T-8.25 N PCHECKCARD PUNJAB RESTAURANT ^ D08/28/2000 T-17.17 N PCHECKCARD SUPERMARKET



    浏览过文件之后,不难猜出其格式 -- 忽略第一行,其余的格式如下:

    D<数据>
    T<交易量>
    N<支票号>
    P<描述>
    ^ (这是字段分隔符)

    开始处理
    在处理象这样重要的 sed 项目时,不要气馁 -- sed 允许您将数据逐渐修改成最终形式。在进行当中,可以继续细化 sed 脚本,直到输出与预期的完全一样为止。无需在试第一次时就保证其完全正确。

    要开始,首先创建一个名为 "qiftrans.sed" 的文件,然后开始修改数据:

    1d /^^/d s/[[:cntrl:]]//g

    第一个 '1d' 命令删除第一行,第二个命令从输出除去那些讨厌的 '^' 字符。最后一行除去文件中可能存在的任何控制字符。既然在处理外来文件格式,我想消除在中途遇到任何控制字符的风险。到目前为止,一切顺利。现在,要向该基本脚本中添加一些处理功能:

    1d /^^/d s/[[:cntrl:]]//g /^D/ {
    s/^D(.*)/1 OUTY INNY /
    s/^01/Jan/ s/^02/Feb/
    s/^03/Mar/ s/^04/Apr/
    s/^05/May/ s/^06/Jun/
    s/^07/Jul/ s/^08/Aug/
    s/^09/Sep/ s/^10/Oct/
    s/^11/Nov/ s/^12/Dec/
    s:^(.*)/(.*)/(.*):2 1 3: }

    首先,添加一个 '/^D/' 地址,以便 sed 只在遇到 QIF 数据字段的第一个字符 'D' 时才开始处理。当 sed 将这样一行读入其模式空间时,将按顺序执行花括号中的所有命令。

    花括号中的第一个命令将把如下行:

    D08/28/2000

    变换成:

    08/28/2000 OUTY INNY

    当然,现在的格式还不完美,但没关系。我们将在进行过程中逐渐细化模式空间的内容。后面 12 行的最后效果是将数据变换成三个字母的格式,最后一行从数据中除去三个斜杠。最后得到这一行:

    Aug 28 2000 OUTY INNY

    OUTY 和 INNY 字段是占位符,以后将被替换。现在还不能确定它们,因为如果美元金额为负,将把 OUTY 和 INNY 设置成 "misc" 和 "-",但是,如果美元金额为正,将分别把它们更改成 "-" 和 "inco"。既然还没有读入美元金额,所以,需要暂时使用占位符。

    细化
    现在进一步细化:

    1d /^^/d s/[[:cntrl:]]//g /^D/ {
    s/^D(.*)/1 OUTY INNY /
    s/^01/Jan/ s/^02/Feb/
    s/^03/Mar/ s/^04/Apr/
    s/^05/May/ s/^06/Jun/
    s/^07/Jul/ s/^08/Aug/
    s/^09/Sep/ s/^10/Oct/
    s/^11/Nov/ s/^12/Dec/
    s:^(.*)/(.*)/(.*):2 1 3:
    N N N
    s/ T(.*) N(.*) P(.*)/NUM2NUM Y 3 AMT1AMT/
    s/NUMNUM/-/ s/NUM([0-9]*)NUM/1/
    s/([0-9]),/1/ }



    后七行有些复杂,所以将详细讨论它们。首先,连续使用三个 'N' 命令。'N' 命令告诉 sed 将下一行读入输入中,然后将其附加到当前模式空间。这三个 'N' 命令导致将下三行附加到当前模式空间缓冲区,现在这一行看起来如下:

    28 Aug 2000 OUTY INNY T-8.15 N PCHECKCARD SUPERMARKET

    sed 的模式空间变得很难看 -- 需要除去额外的新行,并执行某些附加的格式化。要这样做,将使用替代命令。要匹配的模式为:

    ' T.* N.* P.*'

    这将与后面依次跟有 'T'、零或多个字符、新行、'N'、任何数量的字符、新行、'P'、以及任何数量字符的新行匹配。呀!这个规则表达式将与刚刚附加到模式空间的三行的全部内容匹配。但我们要重新格式化该区域,而不是整个替换它。美元金额、支票号(如果有的话)和描述需要出现在替换字符串中。要这样做,我们用带有反斜杠的圆括号括起那些“感兴趣部分”,以便可以在替换字符串中引用它们(使用 '1'、'2 和 '3' 来告诉 sed 将它们插入到何处)。以下是最后的命令:

    s/ T(.*) N(.*) P(.*)/NUM2NUM Y 3 AMT1AMT/

    该命令将我们的行变换成:

    28 Aug 2000 OUTY INNY NUMNUM Y CHECKCARD SUPERMARKET AMT-8.15AMT

    虽然该行正变得好一些,但是,有几件事一看就有点...啊...有趣。首先是那个愚蠢的 "NUMNUM" 字符串 -- 其目的何在?如果查看 sed 脚本的后两行,就会发现其目的,后两行将把 "NUMNUM" 替换成 "-",而把 "NUM"<number>"NUM" 替换成 <number>。如您所见,用愚蠢的标记括起支票号允许我们在该字段为空时方便地插入一个 "-"。

    结束尝试
    最后一行除去数字后的逗号。它把如 "3,231.00" 这样的美元金额转换成我使用的格式 "3231.00"。现在,让我们看一下最终脚本:

    最终的“QIF 到文本”脚本 1d /^^/d s/[[:cntrl:]]//g /^D/ { s/^D(.*)/1 OUTY INNY /
    s/^01/Jan/ s/^02/Feb/ s/^03/Mar/ s/^04/Apr/ s/^05/May/
    s/^06/Jun/ s/^07/Jul/ s/^08/Aug/ s/^09/Sep/ s/^10/Oct/
    s/^11/Nov/ s/^12/Dec/ s:^(.*)/(.*)/(.*):2 1 3:
    N N N s/ T(.*) N(.*) P(.*)/NUM2NUM Y 3 AMT1AMT/
    s/NUMNUM/-/ s/NUM([0-9]*)NUM/1/ s/([0-9]),/1/
    /AMT-[0-9]*.[0-9]*AMT/b fixnegs
    s/AMT(.*)AMT/1/ s/OUTY/-/ s/INNY/inco/
    b done :fixnegs s/AMT-(.*)AMT/1/ s/OUTY/misc/
    s/INNY/-/ :done }

    附加的十一行使用替代和一些分支功能来美化输出。首先看一下这行:

    /AMT-[0-9]*.[0-9]*AMT/b fixnegs

    该行包含一个格式为 "/regexp/b label" 的分支命令。如果模式空间与规则表达式匹配,sed 将分支到 fixnegs 标号。您应该可以轻易找到该标号,它在代码中为 ":fixnegs"。如果规则表达式不匹配,则以常规方式继续处理下一个命令。

    既然您理解该命令本身的工作原理,让我们看一下分支。如果看一下分支规则表达式,将看到它与后面依次跟有 '-'、任意数量的数字、一个 '.'、任意数量的数字和 'AMT' 的字符串 'AMT' 匹配。就象我确信您已猜到一样,该规则表达式专门处理负的美元金额。在这之前,用 'ATM' 括起美元金额,以便以后可以轻易找到它。因为规则表达式只与以 '-' 开始的美元金额匹配,所以,该分支只在恰巧处理借款时才发生。如果正处理贷款,应该将 OUTY 设置成 'misc',将 INNY 设置成 '-',并且应该除去贷款数量前面的负号。如果跟踪代码的流程,将看到实际情况正是这样。如果不执行分支,则用 '-' 替换 OUTY,用 'inco' 替换 INNY。完成了!现在输出行是完美的:

    28 Aug 2000 misc - - Y CHECKCARD SUPERMARKET -8.15

    别犯糊涂
    如您所见,只要循序渐进地解决问题,使用 sed 转换数据就没有那么难。不要试图使用一个 sed 命令或一下子解决所有问题。相反,要朝着目标逐步进行,并不断改进 sed 脚本,直到其输出正如您希望那样为止。sed 有许多功能,希望您已非常熟悉其内部工作原理并继续努力以进一步掌握它!

    网友留言/评论

    我要留言/评论