AWK语言第二版 2.6个人库 2.7小结

2.6 个人库

Awk提供了适量的内置函数库,如 lengthsubsubstrprintf 等其他十来个;在A.2.1节的参考手册中都有列出。你可以自己创建更多函数,以便有需要时引入到Awk程序中。比如内置库函数 sub gsub 都只能返回替换的次数,而你可以用它们来写自己的字符串替换函数,把返回改成替换后的字符串。本节还会给出少量样例,都是这些年来我们认为很有用的。

函数 rest(n) 返回从第n个域开始的所有输入域。

# rest(n): 返回一个字符串,包含 第n到第NF个域,用空格分隔

function rest(n,   s) {
  s = ""
  while (n <= NF)
    s = s $n++ " "
  return substr(s, 1, length(s)-1)  # 删除末尾空格
}

# 测试下:
{ for (i = 0; i<= NF+1; i++)
    printf("%3d [%s]\n", i, rest(i))
}

注意:由于$符号的优先级比++高,s = s $n++ " "  这一行要改为 s = s $(n++) " " 才能正常运行,否则会陷入死循环。原书代码有问题。

函数 rest 有个局部变量 s。Awk中没有变量声明,因此只要是调用者没有提供的参数,都会被当作函数的局部变量(很遗憾,这是语言设计的糟糕之处)。在本例中,调用 rest 的时候只带了一个参数 n ,所以第二个参数 s,就是函数内的局部变量。

按惯例,我们在写函数声明的时候,都会在局部变量名前面多加些空格,这样可以把参数和局部变量区分清楚。还有一种做法是使用容易区分的名字,比如用下划线做前缀或后缀:

function rest(n,   _s) {
  _s = ""
  while (n <= NF)
    _s = _s $(n++) " "
  return substr(_s, 1, length(_s)-1)
}

不过这样看起来会有点难受。

当然还有一种做法,是在所有局部变量前面,加上一个不用的参数,比如 locals 或者下划线_。上面这三种方法都是针对糟糕的语言设计,做出的不完美的变通。

你还可以自己写一些 rest 的变种函数,比如写个 subfields(m, n) 返回从第m 到 第n 的连续域的序列,或者写个 join 把一个数组中的所有值变成空格分隔的序列,或者把一个数组转换成JSON对象:

{"name": "value", ...}

如果你使用标准Awk,那就不得不手动拷贝这些函数到你的程序里面,这实际就是复制粘贴:很简单但是有风险。A.5.4节的参考手册里有个 include 程序。或者可以使用多个 -f 参数来包含多个Awk 源文件。

日期格式化

本章前面2.5节的例子里使用的日期格式为 mm/dd/yy,这是美国的使用习惯,但与其他地方不一样,而且很难对这种格式做排序或其他数学计算。我们能很轻松写出一个 datefix 函数来把这种格式转换成ISO标准格式 yyyy-mm-dd,这样的话数据就能直接用日期来排序了。

# datefix: 把 mm/dd/yy 转换成 yyyy-mm-dd (从1940年到2039年)

awk '
function datefix(s,   y, date) {
  split(s, date, "/")
  y = date[3]<40 ? 2000+date[3] : 1900+date[3]  # 任意年份
  return sprintf("%4d-%02d-%02d", y, date[1], date[2])
}

{ print(datefix($0)) }
' $*

$ datefix
12/25/23
2023-12-25

Awk内置函数 split (s, arr, sep) 用分隔符 sep 将字符串 s 分割到数组 arr 中。元素编号从1开始,split 返回元素的个数。分隔符是正则表达式,可以写成字符串形式如 "sep" ,或是用斜杠包围如 /sep/。如果没有 sep 参数,且传给Awk程序的参数里包含了 --csv,则字符串以CSV格式进行分割;否则,就使用域分隔符变量 FS(见A.5.2节) 来进行分割。有一种特殊情况:如果 sep 是空字符串 "" 或空正则表达式 //,则字符串会被分割成单个字符,即每个数组元素一个字符。

上面的代码用了一个比较随意的规则来将两位数的年份转换成四位数的:如果小于40,则认为是20xx年,否则认为是19xx年。

运算符 ?: 的语法为 表达式1 ? 表达式2 : 表达式3,与C语言一样。它对表达式1求值,如果为真,则结果为表达式2,否则为表达式3;只会对表达式2和3中的一个求值。实际上 ?: 是能用在表达式里面的 if-else 的紧凑写法。它非常方便,但很容易被滥用,造成代码难以读懂。

最后,注意 sprintf 中的转换:%02d 用两位宽度来打印整数,位数不足时在前面补0。

假如我们想要从本地操作系统中获取当前的日期和时间,我们可以使用Unix命令 date,然后将它的返回内容重新格式化。最简单的方法是在Awk里运行 date,并将它的输出管道化给Awk的 getline 函数,这个函数从文件或管道中读取输入:

"date" | getline date    # 获取当前日期和时间
split(date, d, / /)      # 用字符串 " " 也可以
date = d[2] " " d[3] ", " d[6]

只要一点点处理就能将这种日期格式:

Wed Jul 12 07:16:19 EDT 2023

转换成下面的格式:(注意:如果日期是一位数,d[3]会是空字符,d[4]才是日期,年份是[7])

Jul 12, 2023

也可以转换成你想要的任意格式,可能会用上前面的 datefix 函数。

getline 命令和管道的更多细节参见 A.5.4。

假定你要把月份转换成数字,Jan是1,Feb是2,等等。可以使用一系列赋值语句来做,比如m["Jan"]=1, m["Feb"]=2, 以此类推,但这样写起来太繁琐了。一个不错的替代方案是写个函数,把字符串拆到索引数组中,如下:

# isplit - 用str构造索引数组

function isplit(str, arr,   n, i, temp) {
    n = split(str, temp)
    for (i = 1; i <= n; i++)
        arr[temp[i]] = i
    return n
}

 isplit 函数很像 split,区别是它构造出来的数组下标是字符串里面的单词,而对应的值是单词在字符串中的索引。执行如下语句后

isplit("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec", m)

m["Jan"] 的值是1,而m["Dec"]的值是12。

前面说过,split函数可以有第三个参数,是一个正则表达式。你可以对 isplit 做扩展,(以字符串方式)对 isplit 函数传入这个参数。

练习2-5、写个tomorrow脚本,以合适的格式打印出明天的日期。

练习2-6、写出返回修改后的字符串的 sub gsub 版本,类似Python的 re.sub函数。

 2.7 小结

我们在本章展示了一些个人认为有用的脚本。很可能大部分都不是读者直接想要的,但我们希望它们能在你自己写程序时带来一些启发,而且这些样例中展示了不少技巧,会让你编程更轻松。

本章的例子大部分是基于如下内容的组合:计算相关数值的算术表达式,储存信息的数组,以及封装计算的函数。这些机制是编程中至关重要的。在Awk里使用它们特别容易,因为Awk就是围绕它们来设计的,但同样的方法在其他语言里也是无价的,非常值得你花时间来掌握。

第二章完

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