与解析相关的任务
11、清理svn源代码树
Subversion 的 svn status 命令会显示所有被修改过的文件,但如果源代码树中还存在 scratch 文件或其他垃圾,那么 svn 也会一并将其列出。有没有什么实用方法能清理源代码树,删除那些 Subversion 未知的文件?
在使用 svn add 命令前,Subversion 对新文件一无所知。添加新的源文件或者永久删除某些文件之后,再运行该脚本。
你可以用 grep 过滤 svn status 命令的输出,然后用 read命令读取过滤后的结果,以此创建待删除的文件列表。
bash
svn status src | grep '^\?' | \
while read status filename; do echo "$filename"; rm -rf "$filename"; done
在 svn status 的输出中,一个文件占据一行。对于已经修改过的文件,行首字符是 M;对于新添加的文件(但尚未提交),行首字符是 A;未知文件的行首字符是问号。可以用 grep 过滤出以问号起始的那些行。我们在 while 循环中使用 read 语句处理循环。并不是非得用 echo,不过能看到被删除的文件还是有帮助的,省得出现失误或错误。至少你知道哪些文件没了。在删除文件时,我们使用了 -rf 选项,以防要删除的文件是目录,不过这么做主要是图个清静。-f 选项可以消除权限之类的问题,它只会在权限许可的最大范围内删除文件。我们将文件名引用放进引号("$fn"),以防文件名中包含空格之类的特殊字符。
12、用MySQL设置数据库
你想用 MySQL 创建并初始化多个数据库。你希望这些数据库在初始化时全部用相同的 SQL 命令。每个数据库都有自己的名称,但数据库内容全都一样,至少初始化阶段是这样的。你可能得一遍又一遍地重复设置,比如,在这些数据库作为测试套件组成部分的情况下,就需要在重新运行测试时将其重置。
下例中简单的 bash 脚本可以帮助你完成这项管理任务。
bash
#!/usr/bin/env bash
# 实例文件:dbiniter
#
# 通过标准文件初始化数据库
# 根据需要创建数据库
DBLIST=$(mysql -e "SHOW DATABASES;" | tail -n +2)
select DB in $DBLIST "new..."
do
if [[ $DB == "new..." ]]
then
printf "%b" "name for new db: "
read DB rest
echo creating new database $DB
mysql -e "CREATE DATABASE IF NOT EXISTS $DB;"
fi
if [ -n "$DB" ]
then
echo Initializing database: $DB
mysql $DB < ourInit.sql
fi
done
添加 tail -n +2 是为了删除数据库列表的标题部分。
select 用于创建菜单,显示已有的数据库。我们添加了一行 "new..." 来作为额外的选择。
当用户想创建新数据库时,我们可以发出提示并读取数据库名称,但我们在 read 语句中指定了两个变量作为错误处理的一个小措施。如果用户输入了不止一个名称,则只使用第一个并将其保存在变量 $DB 中,其余的内容放进$rest 并忽略。(可以加入错误检查,查看 $rest 是否为空。)
不管是创建新数据库,还是从现有数据库中选择,如果$DB 变量不为空,则调用 mysql 命令,并将作为标准初始化序列保存在文件 ourInit.sql 中的 SQL 语句传给它。
如果打算使用这种脚本,你可能还得给 mysql 命令添加一些参数,如 -u 和 -p,以提示用户名和密码。这取决于数据库及其权限的配置方式,或者 MySQL 的默认配置是否设置了 .my.cnf 文件。
我们还可以加入错误检查,查看新数据库是否成功创建;如果不成功,执行 unset DB,跳过初始化步骤。
13、提取数据中的特定字段
你需要从每行输出中提取一个或多个字段。
如果存在字段分隔符,使用 cut 就能很轻松地办到,哪怕字段首尾的分隔符不一样。
bash
# 这个问题很简单:该NetBSD系统中都有哪些用户,
# 他们的主目录和shell都是什么?
$ cut -d':' -f1,6,7 /etc/passwd
root:/root:/bin/csh
toor:/root:/bin/sh
daemon:/:/sbin/nologin
operator:/usr/guest/operator:/sbin/nologin
bin:/:/sbin/nologin
games:/usr/games:/sbin/nologin
postfix:/var/spool/postfix:/sbin/nologin
named:/var/chroot/named:/sbin/nologin
ntpd:/var/chroot/ntpd:/sbin/nologin
sshd:/var/chroot/sshd:/sbin/nologin
smmsp:/nonexistent:/sbin/nologin
uucp:/var/spool/uucppublic:/usr/libexec/uucp/uucico
nobody:/nonexistent:/sbin/nologin
jp:/home/jp:/usr/pkg/bin/bash
# 该系统中的哪个shell用得最多?
$ cut -d':' -f7 /etc/passwd | sort | uniq -c | sort -rn
10 /sbin/nologin
2 /usr/pkg/bin/bash
1 /bin/csh
1 /bin/sh
1 /usr/libexec/uucp/uucico
# 我们来看看前两级目录
$ cut -d':' -f6 /etc/passwd | cut -d'/' -f1-3 | sort -u
/
/home/jp
/nonexistent
/root
/usr/games
/usr/guest
/var/chroot
/var/spool
如果分隔符包含多个空白字符,或者需要重新安排输出字段的顺序,可以使用 awk。注意,→代表输出中的制表符。默认的输出分隔符是空格,但可以通过 $OFS 修改。
bash
# 用户名、主目录和shell,
# 交换后两个字段并使用制表符作为分隔符
$ awk 'BEGIN {FS=":"; OFS="\t"; } { print $1,$7,$6; }' /etc/passwd
root → /bin/csh → /root
toor → /bin/sh → /root
daemon → /sbin/nologin → /
operator → /sbin/nologin → /usr/guest/operator
bin → /sbin/nologin → /
games → /sbin/nologin → /usr/games
postfix → /sbin/nologin → /var/spool/postfix
named → /sbin/nologin → /var/chroot/named
ntpd → /sbin/nologin → /var/chroot/ntpd
sshd → /sbin/nologin → /var/chroot/sshd
smmsp → /sbin/nologin → /nonexistent
uucp → /usr/libexec/uucp/uucico → /var/spool/uucppublic
nobody → /sbin/nologin → /nonexistent
jp → /usr/pkg/bin/bash → /home/jp
# 多个空白字符,交换第2个和第3个字段,删除第1个字段
$ grep '^# [1-9]' /etc/hosts | awk '{print $3,$2}'
10.255.255.255 10.0.0.0
172.31.255.255 172.16.0.0
192.168.255.255 192.168.0.0
grep -o 只显示匹配指定模式的文本。如果无法像上面那样描述分隔符,这就特别有用了。假设你需要提取文件中出现的所有 IP 地址。注意,我们使用 egrep 是鉴于其正则表达式(regex)功能,但是 -o 选项应该适用于所有GNU grep 版本(非 GNU 版本可能无法使用,可查阅相关文档)。
bash
$ cat has_ipas
This is line 1 with 1 IPA: 10.10.10.10
Line 2 has 2; they are 10.10.10.11 and 10.10.10.12.
Line three is ftp_server=10.10.10.13:21.
$ egrep -o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' has_ipas
10.10.10.10
10.10.10.11
10.10.10.12
10.10.10.13
各种可能性数不胜数,这里列举的甚至连皮毛都算不上。这正是 Unix 工具链思想的本质所在:使用大量专注于做好一件事的小工具,根据需要将其组合来解决各种问题。
另外,处理 IP 地址的正则表达式也并未经过深思熟虑,可能会匹配到包括非法 IP 地址在内的其他内容。如果你所用的 grep 支持 -P,要想取得更好的效果,可以使用 JeffreyE. F. Friedl 所著的《精通正则表达式(第 3 版)》一书中给出的 Perl 兼容正则表达式(Perl compatible regularexpression,PCRE)。
bash
$ grep -oP '([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.
([01]?\d\
d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])' has_ipas
10.10.10.10
10.10.10.11
10.10.10.12
10.10.10.13
$
或者使用 Perl。
bash
$ perl -ne 'while ( m/([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.
([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])/g ) { print qq($1.$2.$3.
$4\n); }' has_ipas
10.10.10.10
10.10.10.11
10.10.10.12
10.10.10.13
$
14、更新数据文件中的特定字段
你需要提取某行(记录)中的特定部分(字段)并更新。
最简单的情况是从一行中提取单个字段,然后对其进行处理。为此,你可以使用 cut 或 awk。
在更复杂的情况中,需要就地修改数据文件的字段。如果只是简单的搜索并替换,可以使用 sed。
例如,我们想将 NetBSD 系统中所有用户的 csh 修改成sh。
bash
$ grep csh /etc/passwd
root:*:0:0:Charlie &:/root:/bin/csh
$ sed 's;/csh$;/sh;' /etc/passwd | grep '^root'
root:*:0:0:Charlie &:/root:/bin/sh
如果需要对字段执行算术运算或修改特定字段中的字符串,可以使用 awk。
bash
$ cat data_file
Line 1 ends
Line 2 ends
Line 3 ends
Line 4 ends
Line 5 ends
$ awk '{print $1, $2+5, $3}' data_file
Line 6 ends
Line 7 ends
Line 8 ends
Line 9 ends
Line 10 ends
# 如果第2个字段中包含'3',将其修改为'8'并做标记
$ awk '{ if ($2 == "3") print $1, $2+5, $3, "Tweaked" ; else print $0; }' \
data_file
Line 1 ends
Line 2 ends
Line 8 ends Tweaked
Line 4 ends
Line 5 ends
你要面对的各种情况就像手里的数据一样无穷无尽,希望这些例子能为你做好充分的准备,在修改数据时事半功倍。
15、修剪空白字符
你需要修剪行首或行尾的空白字符。
下面给出的解决方案依赖于 bash 对待 read 和 $REPLY 的特有处理方式。本节结尾处讨论了另一种解决方案。
首先,我们展示了一个包含行首和行尾空白字符的文件。注意,我们在输出中添加了 ~~ 以表示空白字符,另外用→表示制表符。
bash
# 显示样例文件中的空白字符
$ while read; do echo ~~"$REPLY"~~; done < whitespace
~~ This line has leading spaces.~~
~~This line has trailing spaces. ~~
~~ This line has both leading and trailing spaces. ~~
~~ → Leading tab.~~
~~Trailing tab. → ~~
~~ → Leading and trailing tab. → ~~
~~ → Leading mixed whitespace.~~
~~Trailing mixed whitespace. → ~~
~~ → Leading and trailing mixed whitespace. → ~~
$
用 IFS 和内建的 REPLY 变量修剪行首和行尾的空白字符(要想知道为什么这种方法可行,参见下一节)。
bash
$ while read REPLY; do echo ~~"$REPLY"~~; done < whitespace
~~This line has leading spaces.~~
~~This line has trailing spaces.~~
~~This line has both leading and trailing spaces.~~
~~Leading tab.~~
~~Trailing tab.~~
~~Leading and trailing tab.~~
~~Leading mixed whitespace.~~
~~Trailing mixed whitespace.~~
~~Leading and trailing mixed whitespace.~~
$
要想只修剪行首或行尾的空格,可以使用下面的简单方法。
bash
# 只修剪行首空格
$ while read; do echo "~~${REPLY## }~~"; done < whitespace
~~This line has leading spaces.~~
~~This line has trailing spaces. ~~
~~This line has both leading and trailing spaces. ~~
~~ → Leading tab.~~
~~Trailing tab. ~~
~~ → Leading and trailing tab. → ~~
~~ → Leading mixed whitespace.~~
~~Trailing mixed whitespace. → ~~
~~ → Leading and trailing mixed whitespace. → ~~
# 只修剪行尾空格
$ while read; do echo "~~${REPLY%% }~~"; done < whitespace
~~ This line has leading spaces.~~
~~This line has trailing spaces.~~
~~ This line has both leading and trailing spaces.~~
~~ → Leading tab.~~
~~Trailing tab. ~~
~~ → Leading and trailing tab. → ~~
~~ → Leading mixed whitespace.~~
~~Trailing mixed whitespace. → ~~
~~ → Leading and trailing mixed whitespace. → ~~
只修剪行首或行尾空白字符(包括制表符)的话,那就有点复杂了。
bash
# 需要先完成这一步
$ shopt -s extglob
# 只修剪行首空白字符
$ while read; do echo "~~${REPLY##+([[:space:]])}~~"; done < whitespace
~~This line has leading spaces.~~
~~This line has trailing spaces. ~~
~~This line has both leading and trailing spaces. ~~
~~Leading tab.~~
~~Trailing tab. ~~
~~Leading and trailing tab. → ~~
~~Leading mixed whitespace.~~
~~Trailing mixed whitespace. → ~~
~~Leading and trailing mixed whitespace. → ~~
$
# 只修剪行尾空白字符
$ while read; do echo "~~${REPLY%%+([[:space:]])}~~"; done < whitespace
~~ This line has leading spaces.~~
~~This line has trailing spaces.~~
~~ This line has both leading and trailing spaces.~~
~~ → Leading tab.~~
~~Trailing tab.~~
~~ → Leading and trailing tab.~~
~~ → Leading mixed whitespace.~~
~~Trailing mixed whitespace.~~
~~ → Leading and trailing mixed whitespace.~~
此时你可能正盯着这些代码,好奇我们打算怎么样将来龙去脉讲清楚。尽管的确是有些微妙之处,但解释起来并不复杂。
开始吧。第一个示例用到了 REPLY 变量,read 会在用户未指定变量时使用该默认变量。这个设计决定是由 ChetRamey(bash 的维护人员)做出的:"(如果)没有指定变量,就将读入的文本行保存在 REPLY 变量之中", 现在依然如此。
bash
while read; do echo ~~"$REPLY"~~; done < whitespace
但如果为 read 指定了一个或多个变量,那么它会使用$IFS 中的值(默认是空格、制表符以及换行符)来解析输入。解析过程的其中一步就是修剪行首和行尾的空白字符,这正如我们所愿。
bash
while read REPLY; do echo ~~"$REPLY"~~; done < whitespace
可以通过 {##} 或 {%%} 运算符(参见 6.7 节)轻松修剪行首或行尾空格(二选一)。
bash
while read; do echo "~~${REPLY## }~~"; done < whitespace
while read; do echo "~~${REPLY%% }~~"; done < whitespace
转换制表符有点难度。如果单纯是制表符,可以使用{##} 或 {%%} 运算符并通过 Ctrl-V Ctrl-I 组合键序列插入字面制表符(literan tab)。但这么做有风险,因为可能既有空格也有制表符,而且有些文本编辑器或粗心的用户可能会剔除制表符。因此,我们启用扩展通配符匹配,通过字符类来表明意图。字符类 :space: 不需要 extglob就可以使用,但我们得用 +() 表明"一次或多次出现",否则就只能修剪掉单个空格或制表符,无法顾及多个。如果只在意空格或制表符,也可以使用 :blank: 来代替,因为 :space: 还包括垂直制表符(\v)和 DOS CR(回车,\r)在内的其他字符。
bash
# 要想生效,+()部分需要启用extglob
$ shopt -s extglob
...
$ while read; do echo "~~${REPLY##+([[:space:]])}~~"; done < whitespace
...
$ while read; do echo "~~${REPLY%%+([[:space:]])}~~"; done < whitespace
...
# 不管用
$ while read; do echo "~~${REPLY##[[:space:]]}~~"; done < whitespace
~~This line has leading spaces.~~
~~This line has trailing spaces. ~~
~~This line has both leading and trailing spaces. ~~
~~Leading tab.~~
~~Trailing tab. ~~
~~Leading and trailing tab. ~~
~~ → Leading mixed whitespace.~~
~~Trailing mixed whitespace. → ~~
~~ → Leading and trailing mixed whitespace. → ~~
另一种方法也是利用 $IFS 进行解析,但解析出的是字段(或单词),而不是记录(或行)。
bash
$ for i in $(cat white_space); do echo ~~$i~~; done
~~This~~
~~line~~
~~has~~
~~leading~~
~~white~~
~~space.~~
~~This~~
~~line~~
~~has~~
~~trailing~~
~~white~~
~~space.~~
~~This~~
~~line~~
~~has~~
~~both~~
~~leading~~
~~and~~
~~trailing~~
~~white~~
~~space.~~
$
最后,虽然最初的解决方案依赖于 Chet 有关 read 和$REPLY 的设计决定,但下面这种解决方案并不需要。
bash
shopt -s extglob
while IFS= read -r line; do
echo "None: ~~$line~~" # 保留所有的空白字符
echo "Ld: ~~${line##+([[:space:]])}~~" # 修剪行首空白字符
echo "Tr: ~~${line%%+([[:space:]])}~~" # 修剪行尾空白字符
line="${line##+([[:space:]])}" # 修剪行首和......
line="${line%%+([[:space:]])}" # ......行尾的空白字符
echo "All: ~~$line~~" # 显示修剪后的内容
done < whitespace
16、压缩空白字符
你的文件中有一些连续的空白字符(可能是用空格填充的固定长度记录),你需要将这些空白字符压缩成单个字符或分隔符。
根据需要使用 tr 或 awk。
如果你尝试将连续的空白字符压缩成单个字符,可以使用tr,但要注意,这可能会破坏格式不佳的文件。例如,如果字段之间是由多个空格分隔,但字段内部也包含空格,将多个空格压缩成单个会使得这种差异荡然无存。假设下例用空格代替 _。注意,→表示输出中的制表符。
bash
$ cat data_file
Header1 Header2 Header3
Rec1_Field1 Rec1_Field2 Rec1_Field3
Rec2_Field1 Rec2_Field2 Rec2_Field3
Rec3_Field1 Rec3_Field2 Rec3_Field3
$ cat data_file | tr -s ' ' '\t'
Header1 → Header2 → Header3
Rec1_Field1 → Rec1_Field2 → Rec1_Field3
Rec2_Field1 → Rec2_Field2 → Rec2_Field3
Rec3_Field1 → Rec3_Field2 → Rec3_Field3
如果字段分隔符不止一个字符,那 tr 就没办法了,因为它是将第一个集合中的单个字符转换成第二个集合中相匹配的单个字符。你可以用 awk 来组合或转换字段分隔符。awk 的内部字段分隔符 FS 接受正则表达式,因此你能够用其分隔的字段可就太多了。另外还有一个很方便的技巧:给任意字段赋值会使得 awk 用输出字段分隔符 OFS重组记录,因此字段 1 自己给自己赋值,然后再输出记录,其效果相当于将 FS 转换成了 OFS,而且也不用担心数据中有多少条记录。
在这个例子中,分隔字段的是多个空格,但字段本身也包含内部空格,因此下面这种简单的处理方法并不管用。
bash
awk 'BEGIN {OFS="\t"} {$1=$1; print }' data_file1
数据文件如下所示。
bash
$ cat data_file1
Header1 Header2 Header3
Rec1 Field1 Rec1 Field2 Rec1 Field3
Rec2 Field1 Rec2 Field2 Rec2 Field3
Rec3 Field1 Rec3 Field2 Rec3 Field
$
接下来的示例将两个空格赋给 FS,一个制表符赋给OFS。然后执行赋值操作(1 = 1),因此 awk 会重建记录,从而令两个空格被一连串的制表符替换,因此我们又用 gsub 压缩制表符,然后输出。注意,→代表输出中的制表符。由于整个输出的可读性有点不太好,我们同时给出了 16 进制格式。再回忆一下,制表符和空格的 ASCII编码值分别是 09 和 20。
bash
$ awk 'BEGIN { FS = " "; OFS = "\t" } { $1 = $1; gsub(/\t+ ?/, "\t"); print }' \
data_file1
Header1 → Header2 → Header3
Rec1 Field1 → Rec1 Field2 → Rec1 Field3
Rec2 Field1 → Rec2 Field2 → Rec2 Field3
Rec3 Field1 → Rec3 Field2 → Rec3 Field3
$ awk 'BEGIN { FS = " "; OFS = "\t" } { $1 = $1; gsub(/\t+ ?/, "\t"); print }' \
data_file1 | hexdump -C
00000000 48 65 61 64 65 72 31 09 48 65 61 64 65 72 32 09 |Header1.Header2.|
00000010 48 65 61 64 65 72 33 0a 52 65 63 31 20 46 69 65 |Header3.Rec1 Fie|
00000020 6c 64 31 09 52 65 63 31 20 46 69 65 6c 64 32 09 |ld1.Rec1 Field2.|
00000030 52 65 63 31 20 46 69 65 6c 64 33 0a 52 65 63 32 |Rec1 Field3.Rec2|
00000040 20 46 69 65 6c 64 31 09 52 65 63 32 20 46 69 65 | Field1.Rec2 Fie|
00000050 6c 64 32 09 52 65 63 32 20 46 69 65 6c 64 33 0a |ld2.Rec2 Field3.|
00000060 52 65 63 33 20 46 69 65 6c 64 31 09 52 65 63 33 |Rec3 Field1.Rec3|
00000070 20 46 69 65 6c 64 32 09 52 65 63 33 20 46 69 65 | Field2.Rec3 Fie|
00000080 6c 64 0a |ld.|
00000083
你可以用 awk 按照相同的方法修剪行首和行尾的空白字符,不过之前提到过,这么做会替换字段分隔符,除非它们已经是空格。
bash
awk '{ $1 = $1; print }' white_space
17、处理固定长度记录
你需要读取并处理的数据采用的是固定长度(也称为固定宽度)格式。
使用 Perl 或 gawk 2.13 及更高版本。样例文件如下所示。
bash
$ cat fixed-length_file
Header1-----------Header2-------------------------Header3---------
Rec1 Field1 Rec1 Field2 Rec1 Field3
Rec2 Field1 Rec2 Field2 Rec2 Field3
Rec3 Field1 Rec3 Field2 Rec3 Field3
你可以用 GNU 的 gawk 来处理:将 FIELDWIDTHS 设置为正确的字段宽度,根据需要设置 OFS,然后执行赋值操作,以便 gawk 用 OFS 重建记录。但是,gawk 并不会删除原始记录中用于填充各个字段的空格,因此我们得用两个 gsub 来解决这个问题:一个处理所有内部字段,另一个处理最后一个字段。最后一步就是输出了。注意,→代表输出中的制表符。由于整个输出的可读性有点不太好,我们同时给出了十六进制格式。再回忆一下,制表符和空格的 ASCII 编码值分别是 09 和 20。
bash
$ gawk 'BEGIN { FIELDWIDTHS = "18 32 16"; OFS = "\t" }
> { $1 = $1; gsub(/ +\t/, "\t"); gsub(/ +$/, ""); print }' fixed-length_file
Header1----------- → Header2------------------------- → Header3---------
Rec1 Field1 → Rec1 Field2 → Rec1 Field3
Rec2 Field1 → Rec2 Field2 → Rec2 Field3
Rec3 Field1 → Rec3 Field2 → Rec3 Field3
$ gawk 'BEGIN { FIELDWIDTHS = "18 32 16"; OFS = "\t" }
> { $1 = $1; gsub(/ +\t/, "\t"); gsub(/ +$/, ""); print }' fixed-length_file \
> | hexdump -C
00000000 48 65 61 64 65 72 31 2d 2d 2d 2d 2d 2d 2d 2d 2d |Header1---------|
00000010 2d 2d 09 48 65 61 64 65 72 32 2d 2d 2d 2d 2d 2d |--.Header2------|
00000020 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d |----------------|
00000030 2d 2d 2d 09 48 65 61 64 65 72 33 2d 2d 2d 2d 2d |---.Header3-----|
00000040 2d 2d 2d 2d 0a 52 65 63 31 20 46 69 65 6c 64 31 |----.Rec1 Field1|
00000050 09 52 65 63 31 20 46 69 65 6c 64 32 09 52 65 63 |.Rec1 Field2.Rec|
00000060 31 20 46 69 65 6c 64 33 0a 52 65 63 32 20 46 69 |1 Field3.Rec2 Fi|
00000070 65 6c 64 31 09 52 65 63 32 20 46 69 65 6c 64 32 |eld1.Rec2 Field2|
00000080 09 52 65 63 32 20 46 69 65 6c 64 33 0a 52 65 63 |.Rec2 Field3.Rec|
00000090 33 20 46 69 65 6c 64 31 09 52 65 63 33 20 46 69 |3 Field1.Rec3 Fi|
000000a0 65 6c 64 32 09 52 65 63 33 20 46 69 65 6c 64 33 |eld2.Rec3 Field3|
000000b0 0a |.|
000000b1
我们使用了 -n 选项(nonprinting),该选项在循环处理输入时不会自动输出每一行(记录),它会拆解(unpack)读入的每条记录($_),然后使用制表符将生成的列表中的各个元素连接成一个标量(scalar)。最后,输出记录并在结尾添加换行符。
bash
$ perl -ne 'print join("\t", unpack("A18 A32 A16", $_) ) . "\n";' \
> fixed-length_file
Header1----------- → Header2------------------------- → Header3---------
Rec1 Field1 → Rec1 Field2 → Rec1 Field3
Rec2 Field1 → Rec2 Field2 → Rec2 Field3
Rec3 Field1 → Rec3 Field2 → Rec3 Field3
$ perl -ne 'print join("\t", unpack("A18 A32 A16", $_) ) . "\n";' \
> fixed-length_file |
> hexdump -C
00000000 48 65 61 64 65 72 31 2d 2d 2d 2d 2d 2d 2d 2d 2d |Header1---------|
00000010 2d 2d 09 48 65 61 64 65 72 32 2d 2d 2d 2d 2d 2d |--.Header2------|
00000020 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d |----------------|
00000030 2d 2d 2d 09 48 65 61 64 65 72 33 2d 2d 2d 2d 2d |---.Header3-----|
00000040 2d 2d 2d 2d 0a 52 65 63 31 20 46 69 65 6c 64 31 |----.Rec1 Field1|
00000050 09 52 65 63 31 20 46 69 65 6c 64 32 09 52 65 63 |.Rec1 Field2.Rec|
00000060 31 20 46 69 65 6c 64 33 0a 52 65 63 32 20 46 69 |1 Field3.Rec2 Fi|
00000070 65 6c 64 31 09 52 65 63 32 20 46 69 65 6c 64 32 |eld1.Rec2 Field2|
00000080 09 52 65 63 32 20 46 69 65 6c 64 33 0a 52 65 63 |.Rec2 Field3.Rec|
00000090 33 20 46 69 65 6c 64 31 09 52 65 63 33 20 46 69 |3 Field1.Rec3 Fi|
000000a0 65 6c 64 32 09 52 65 63 33 20 46 69 65 6c 64 33 |eld2.Rec3 Field3|
000000b0 0a |.|
000000b1
因为脑子里时刻都牢记着 textutils 工具链 5,任何拥有Unix 背景的人都会自觉在输出中使用某种分隔符,所以固定长度(或称作固定宽度)的记录在 Unix 世界里极少见到。不过,这种记录在大型机(mainframe)世界倒是司空见惯,它们偶尔会从源自巨型服务器的大型应用程序中蹦出来,比如 SAP 的某些应用程序。我们之前已经见识过了,固定长度记录处理起来没有任何问题。
textutils 即 GNU Text Utilities,是 GNU 操作系统中用于文本处理的一系列基本工具。fileutils、shellutils 以及textutils 现在已经合并成 GNU Coreutils。
有一处地方要特别注意,这则实例要求每条记录都以换行符作结。但很多陈旧的大型机记录格式可不是这样,对于这种情况,可以使用 13.18 节中的方法,先在每条记录结尾添加换行符,然后再处理。
18、处理没有换行的文件
你手头有一个比较大的文件,其中没有任何换行,现在需要处理该文件。
先对文件进行预处理,在适当的位置添加换行符。例如,OpenOffice 的 ODF 文件基本上就是经过压缩的 XML 文件。可以将其解压,然后用 grep 搜索 XML 文件,我们在撰写本书时没少这么做。有关 ODF 文件的更多处理,参见12.5 节。本例在每个结尾尖括号(>)之后插入一个换行符。这样一来,使用 grep 或其他 textutils 工具处理该文件时就容易多了。注意,我们必须紧跟着反斜线之后按下回车键,以此在 sed 脚本中嵌入一个经过转义的换行符。
bash
$ wc -l content.xml
1 content.xml
$ sed -e 's/>/>\
> /g' content.xml | wc -l
1687
如果有不带换行符的固定长度记录,也可以这么处理,其中 48 是记录长度。
bash
$ cat fixed-length
Line_1_ _aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZLine_2__
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZLine_3__
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZLine_4__
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZLine_5__
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZLine_6__
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZLine_7__
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZLine_8__
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZLine_9__
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZLine_10_
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZLine_11_
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZLine_12_
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
$ wc -l fixed-length
1 fixed-length
$ sed 's/.\{48\}/&\
> /g;' fixed-length
Line_1__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_2__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_3__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_4__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_5__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_6__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_7__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_8__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_9__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_10_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_11_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_12_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
$ perl -pe 's/(.{48})/$1\n/g;' fixed-length
Line_1__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_2__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_3__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_4__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_5__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_6__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_7__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_8__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_9__aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_10_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_11_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
Line_12_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZZZ
当人们以编程方式创建输出时,这种情况经常发生,尤其使用现成模块(canned module),而且还是采用 HTML或 XML 输出时。
注意,sed 的替换功能有一种允许嵌入换行符的怪异写法。在 sed 中,替换运算符右侧(righthand side,RHS)的 & 会被左侧(lefthand side,LHS)的整个正则表达式所匹配到的字符串替换掉,第一行结尾处的 \ 用于转义换行符,否则会出现类似于"sed: -e expression #1,char 11: unterminated's'"这样的错误。这是因为 sed无法将 s/// 右侧的 \n 识别为元字符。
19、将数据文件转换为CSV
你需要将数据文件转换为 CSV(comma separatedvalue,逗号分隔值)文件。
使用 awk 将数据文件转换为 CSV 格式。
bash
$ awk 'BEGIN { FS="\t"; OFS="\",\"" } { gsub(/"/, "\"\""); $1 = $1;
> printf "\"%s\"\n", $0}' tab_delimited
"Line 1","Field 2","Field 3","Field 4","Field 5 with ""internal"" double-quotes"
"Line 2","Field 2","Field 3","Field 4","Field 5 with ""internal"" double-quotes"
"Line 3","Field 2","Field 3","Field 4","Field 5 with ""internal"" double-quotes"
"Line 4","Field 2","Field 3","Field 4","Field 5 with ""internal"" double-quotes"
$
也可以用 Perl 实现相同的效果。
bash
$ perl -naF'\t' -e 'chomp @F; s/"/""/g for @F; print q(").join(q(","),@F)
> .qq("\n);' tab_delimited
"Line 1","Field 2","Field 3","Field 4","Field 5 with ""internal"" double-quotes"
"Line 2","Field 2","Field 3","Field 4","Field 5 with ""internal"" double-quotes"
"Line 3","Field 2","Field 3","Field 4","Field 5 with ""internal"" double-quotes"
"Line 4","Field 2","Field 3","Field 4","Field 5 with ""internal"" double-quotes"
$
首先,明确定义什么是 CSV 可不是件容易事。不存在正式的规范,各个厂商都有自己的一套实现。我们实现的版本非常简单,应该在任何地方都管用。我们给所有的字段都加上双引号(有些实现仅为字符串或包含内部逗号的字符串加引号),如果字段内部已经有双引号,我们则在其之外再加上一层引号。
为此,通过指定制表符为字段分隔符并将 OFS 设置为",",我们用 awk 分割输入行,这样就可以在每个字段的结尾和下一个字段的起始位置加上引号,同时在两者之间添加逗号。然后使用两个双引号替换掉所有的双引号,接着执行赋值操作,令 awk 使用指定好的 OFS 重建记录(该技巧参见 13.15 节),最后输出带有行首和行尾双引号的记录。我们必须在多处转义双引号,这导致看起来有点杂乱,否则还是挺直观易懂的。
20、解析CSV数据文件
需要解析一个 CSV 数据文件。
不同于先前将数据文件转换为 CSV,解析 CSV 可不是一件容易事,因为 CSV 的确切定义都是个问题。
可能的解决方案如下所示。
- sed
- awk
- Perl:Jeffrey E. F. Friedl 所著的《精通正则表达式(第 3 版)》一书中给出了相应的正则表达式。另外,CPAN(Comprehensive Perl Archive Network)中也有各种专用于此的模块。使用电子表格软件(LibreOffice Calc 和 Microsoft Excel 均可)打开 CSV 文件,然后将内容复制粘贴到文本编辑器中,这样应该能得到以制表符分隔的输出,然后就可以轻松处理了。
如 13.19 节中所言,CSV 还没有正式的规范,再加上数据的多样性,这项任务要比听起来困难得多。