AWK语言第二版 2.3转换

2.3 转换

把输入转换到输出是计算机做的事,但Awk做的是一种特定的转换:输入文本,对所有或部分行做些适当的修改,然后退出。

回车

对于如何结束一行,Windows令人遗憾地选择了与macOS和Unix不一样的实现方式(这完全没有必要)。在Windows上,每个文本行以一个回车符 \r 和一个换行符 \n 结束,而macOS和Unix只要一个换行符。出于各种充分的理由(包括作者们从Unix早期以来就开始积累的经验和接受的熏陶),Awk使用了“仅换行符”的模型,尽管输入是Windows格式,它还是能正确处理。

我们可以使用Awk的文本替换函数 sub,来将每行末尾的 \n 干掉。函数 sub(re, repl, str) 会将 str 中匹配第一个正则表达式 re 的内容,替换成文本 repl。如果没有 str 参数,则会对 $0 进行匹配和替换操作。因此下面的程序

{ sub(/\r$/, ""); print }

会将所有行末的回车符号去掉,并输出该行。

还有个函数 gsub 也类似,但会将所有匹配 re 的内容都进行替换,而不仅仅替换第一个; g 代表“全局的”(global)。sub 和 gsub 都会返回替换的次数,这样你就能知道字符串 str 是否有改变。

反过来的话,也很容易在每个换行符前面加上回车,如果本来没有的话:

{ if (!/\r$/) sub(/$/, "\r"); print }

正则表达式 /\r$/ 如果不匹配的话,说明行末没有回车符,就会进行替换操作。正则表达式在附录 A.1.4中会详细说明。

多列

下一例子是将输入内容用多列的方式来输出,当然这里假定输入的每行都很短,像是文件名或者人名。比如下面有一列的人名

Alice
Archie
Eva
Liam
Louis
Mary
Naomi
Rafael
Sierra
Sydney

会转换成

Alice   Archie  Eva     Liam    Louis   Mary   Naomi
Rafael  Sierra  Sydney

这个程序展现出在程序设计的方案选择上的多样性,接下来我们会探究其中的两个。其他的可以当作很好的练习,特别是当你的需求和作者不一样时。

一个基本的选择,是将所有输入都读完,以便搞清楚输入范围?还是边读边输出(流处理的概念),而不管后面的输入会是什么样?

另一个选择,是以行的顺序(作者选了这个),还是以列的顺序进行输出呢?如果是后者,那么在读完所有输出之前,不能进行任何输出。

先来写流的版本。假定输入行不超过10个字符宽,那么算上每列之间加2个空格,一行放5列的话,不会超过60个字符(5*10+4*2= 58)。针对太长的行,我们的选择也很多,比如截断(可以给提示也可以不给),或者在中间加省略号,或者可以拆成多列来打。还能把这些选择作为程序的参数,供用户选择,不过对这个简单的例子来说就实在过头了,没有必要。

下面这个流版本,会默默地截断超长行;可能是最简单的版本了:

# mc: 多列打印的流版本

{ out = sprintf("%s%-10.10s  ", out, $0)
  if (n++ > 5) {
    print substr(out, 1, length(out)-2)
    out = ""
    n = 0
  }
}

END {
  if (n > 0)
    print substr(out, 1, length(out)-2)
}

sprintf 是 printf 的一个特殊版本,它不会将格式化的字符串打印到输出流,而是把它作为函数返回值。其中的格式化字符串 %-10.10s,用来将字符串截断在10个字符宽度内并保持左对齐。

第二行  if (n++ > 5)   展示了 ++ 操作符一个细微但非常重要的特性。如果 ++ 作为变量的后缀,则对应表达式的值是该变量递增之前的值;表达式求值之后,变量才会递增。而如果作为前缀,则会先进行递增,再返回(递增后的)值。【所以老爷子是故意在这里写个有缺陷的代码?前面明明说是想输出5列的,这里却输出了7列】

下面这个版本,会收集所有的输入,以计算出最宽的域,并使用该宽度值来构造合适的 printf 字符串,将字符格式化输出到保证够宽的列中。注意其中 %% 在格式化字符串中表示 %本身。

# mc:多行打印

{ lines[NR] = $0
  if (length($0) > max)
    max = length($0)
}
END {
  fmt = sprintf("%%-%d.%ds", max, max) # 构造格式化字符串
  ncol = int(60 / max + 0.5)  # int(x) 会返回 x 的整数值
  for (i = 1; i <= NR; i += ncol) {
    out = ""
    for (j = i; j < i+ncol && j <= NR; j++)
      out = out sprintf(fmt, lines[j]) "  "
    sub(/ +$/, "", out)  # 删掉行尾的空格
    print out
  }
}

其中这三行会将格式化后的原行以及两个空格加入到 out 的后面,这样就构成了输出行。

for (j = i; j < i+ncol && j <= NR; j++)
  out = out sprintf(fmt, lines[j]) "  "
sub(/ +$/, "", out)  # 删掉行尾的空格

循环结束后,sub 将行末的空格去掉,利用 / +$/ 这个正则表达式匹配行末的一个或多个空格。当然你也许会觉得一开始就不应该加上行末的空格,但这样你得在for循环里加判断代码,还更麻烦。所以我们在两个版本中都采用这种方法。【这个版本的ncol计算也有问题,没把行之间的2个空格算上,难道也是老爷子留的彩蛋?】

练习2-3. 实现多行程序的参数化(即 全部读完再处理还是流式处理,按行顺序还是列顺序, 针对超长和的处理,比如截断(可以给提示也可以不给),或者在中间加省略号,或者可以拆成多列来打)

你可能感兴趣的:(AWK,linux,开发语言)