第九章 函数

算术函数

表9‑1 awk内置算术函数

awk函数 描述
cos(x) 返回x的余弦,x单位是弧度
exp(x) 返回自然常数 e 的 x 次幂
int(x) 截取整数部分,直接丢掉小数,不做四舍五入
log(x) 返回x自然对数(以e为底)
sin(x) 返回x的正弦,x单位是弧度
sqrt(x) 返回x的平方根(x≥0)
atan2(y, x) 返回 y/x 的反正切,结果范围 -π ~ π
rand() 返回伪随机浮点数,范围:0 ≤ r < 1,无参数
srand(x) 设置rand()随机数种子;不带参数使用系统时间做种子;返回旧种子值

三角函数注意点

  • sin()、cos() 参数是弧度,不是角度。角度转弧度公式:角度 * (pi / 180)
  • atan2(0, -1) → 得到 π
  • exp(1) → 得到 e ≈ 2.71828

int() 整数截取

只是截断小数,不是四舍五入;想要四舍五入用printf的%.0f格式

1
2
3
4
BEGIN{
    print 100/3
    print int(100/3)
}

输出:

1
2
33.3333
33

随机数 rand() / srand()

  1. 如果脚本不调用srand():awk启动使用固定种子,每次运行rand()出来的随机序列完全一样,适合测试,不适合真正随机。
  2. srand()不带参数:使用当前系统时间作为种子,每次运行得到不同随机序列。
  3. srand(数字):手动指定固定种子,可以复现同一套随机结果。

示例 rand.awk

1
2
3
4
5
6
7
BEGIN {
    print rand()
    print rand()
    srand()
    print rand()
    print rand()
}

前两次rand()是默认固定种子,每次运行结果不变;执行srand()之后随机序列改变。

生成指定范围随机数公式

获取 1 ~ TOPNUM 之间的整数:

1
select = 1 + int(rand() * TOPNUM)

拆解:

  1. rand() → 0 ~ 0.99999
  2. rand() * TOPNUM → 0 ~ TOPNUM‑0.99999
  3. int(...) → 取整数部分 0 ~ TOPNUM‑1
  4. +1 → 最终范围 1 ~ TOPNUM

简单示例 pick1.awk

1
2
3
4
5
6
awk -v TOPNUM=$1 '
BEGIN {
    srand()
    select = 1 + int(rand() * TOPNUM)
    print select
}'

调用示例:awk -v TOPNUM=100 '...' 输出1‑100随机整数。

lotto彩票选号示例(去重随机数)

需求:选出NUM个1~TOPNUM之间不重复随机数。
核心技巧:用数组pick[select]保存已经选出来的数字;用select in pick判断数字是否已经存在,如果存在就重新生成。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
awk -v NUM=$1 -v TOPNUM=$2 '
BEGIN {
    if (NUM <= 0) NUM = 6
    if (TOPNUM <= 0) TOPNUM = 30
    printf("Pick %d of %d\n", NUM, TOPNUM)
    srand()
    for (j = 1; j <= NUM; ++j) {
        do {
            select = 1 + int(rand() * TOPNUM)
        } while (select in pick)
        pick[select] = select
    }
    for (j in pick)
        printf("%s ", pick[j])
    printf("\n")
}'

缺陷:输出结果无序;原书后续自定义sort函数解决排序。 补充:把pick数组转普通数字下标数组,方便排序:

1
2
3
i=1
for(j in pick)
    sortedpick[i++] = pick[j]

字符串函数

表9‑2 awk内置字符串函数

awk函数 描述
gsub(r,s,t) 在t中将正则r全部替换为s;返回替换次数;省略t默认$0
index(s,t) 返回子串t在s的起始位置,找不到返回0;字符串位置从1开始
length(s) 返回字符串s字符长度;省略参数默认$0
match(s,r) 在s匹配正则r,返回匹配起始位置;无匹配返回0;设置内置变量RSTART、RLENGTH
split(s,a,sep) 用分隔符sep把s切分到数组a;返回元素个数;省略sep使用FS
sprintf(fmt,expr…) 和printf格式一样,不打印,返回格式化后的字符串,可以赋值变量
sub(r,s,t) 在t把正则r第一次匹配替换成s;成功返回1,失败返回0;省略t默认$0
substr(s,p,n) 从s的第p位截取子串;n是截取最大长度;省略n截取到字符串末尾
tolower(s) 返回全部转小写的新字符串
toupper(s) 返回全部转大写的新字符串

awk字符串下标从1开始,和C语言从0开始不一样!

sprintf()

printf直接输出;sprintf()生成字符串赋值变量。 示例,数字转ASCII字符:

1
2
3
4
5
6
BEGIN{
    for(i=97;i<=122;i++){
        nextletter = sprintf("%c",i)
        print nextletter
    }
}

index()、substr() 截取子串

  • index(s,t):查找子串,返回位置,找不到返回0
1
2
3
4
BEGIN{
    pos = index("mississippi", "is")
    print pos  #输出2
}
  • substr(s,p):从p位置截取到末尾
1
2
phone = substr("707-555-1111",5)
print phone  #555-1111
  • substr(s,p,n):截取n个字符
1
2
area_code = substr("707-555-1111",1,3)
print area_code #707

示例脚本caps.awk:把第一个单词首字母大写

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
awk '
BEGIN {
    upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    lower = "abcdefghijklmnopqrstuvwxyz"
}
{
    FIRSTCHAR = substr($1,1,1)
    CHAR = index(lower,FIRSTCHAR)
    if (CHAR != 0){
        $1 = substr(upper, CHAR, 1) substr($1, 2)
    }
    print $0
}'

length() 获取字符串长度

1
2
length($0)   #获取当前整行字符数
length()     #不传参等价 length($0)

常用于判断行是否超过80字符,做文本断行处理。

sub() / gsub() 替换函数

  1. sub():只替换第一次匹配,成功返回1,失败0
  2. gsub():全局全部替换,返回替换发生的总次数
  • 第三个参数t可选,不写默认操作$0
  • 替换字符串里面&代表匹配到的原文;想要字面&需要写\\&
  • awk没有sed那种空//复用上次正则写法!

示例:替换全部UNIX为POSIX,只有发生替换才打印行

1
2
3
4
{
    if (gsub(/UNIX/, "POSIX"))
        print
}

示例:troff字体替换

1
gsub(/UNIX/, "\\fB&\\fR")

输入the UNIX operating system →输出 the \fBUNIX\fR operating system。

tolower()、toupper()大小写转换

返回新字符串,不会修改原字符串,需要赋值覆盖。

1
awk '{ printf("<%s>, <%s>\n", tolower($0), toupper($0)) }' test

match()正则匹配,RSTART / RLENGTH

match(字符串,正则)

  • 返回匹配起始位置;匹配失败返回0
  • 匹配成功自动设置两个内置变量:
    • RSTART:匹配开始下标
    • RLENGTH:匹配内容的字符长度;失败RLENGTH=-1

示例:提取行内匹配到的子串

1
2
3
4
awk '
match($0, pattern) {
    print substr($0, RSTART, RLENGTH)
}' pattern="$1" file.txt

注意参数顺序!match(s,r),第一个参数字符串,第二个参数正则,和sub/gsub顺序相反,容易踩坑。

利用match循环提取所有大写连续字母片段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
awk '
BEGIN {
    upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    lower = "abcdefghijklmnopqrstuvwxyz"
}
{
    while(match($0, /[A-Z]+/)){
        for(x=RSTART; x < RSTART+RLENGTH; x++){
            CAP = substr($0,x,1)
            CHAR = index(upper,CAP)
            gsub(CAP, substr(lower,CHAR,1))
        }
    }
    print $0
}' test

自定义函数

awk允许自己写函数,提高代码复用。函数可以放在脚本任意位置,习惯写在脚本最开头。

语法格式

1
2
3
4
function 函数名(参数列表) {
    语句
    return 表达式   # return返回值;没有return返回空
}

示例:insert(),在字符串指定位置后面插入子串

1
2
3
4
5
function insert(STRING, POS, INS, before_tmp, after_tmp) {
    before_tmp = substr(STRING, 1, POS)
    after_tmp = substr(STRING, POS + 1)
    return before_tmp INS after_tmp
}

调用:

1
2
3
{
    print insert($1,4,"XX")
}

输入Hello →输出HellXXo

awk函数变量作用域(非常重要)

  1. 写在函数参数列表里的变量,是局部变量,函数外部访问不到。
  2. 函数体内直接创建的变量,默认是全局变量,脚本全部地方都可以访问。
  3. awk没有专门local关键字;局部临时变量,写在参数列表的末尾,和真实参数空格隔开。

上面例子中before_tmp, after_tmp仅仅放在参数列表,调用时不传值,它们就成为函数内部局部变量。

  1. 普通标量(数字、字符串)按值传递:函数拿到副本,不会修改外面原始变量。
  2. 数组是按引用传递:函数内部修改数组,外部原始数组同步被改变!

编写排序函数(插入排序)

数组传引用,函数内部直接修改原数组,不需要return返回数组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# ARRAY待排序数组,ELEMENTS元素个数;temp,i,j是局部临时变量
function sort(ARRAY, ELEMENTS, temp, i, j) {
    for (i = 2; i <= ELEMENTS; ++i) {
        for (j = i; (j-1) in ARRAY && ARRAY[j-1] > ARRAY[j]; --j) {
            temp = ARRAY[j]
            ARRAY[j] = ARRAY[j-1]
            ARRAY[j-1] = temp
        }
    }
    return
}

调用方式:

1
2
#sortedpick数组,一共NUM个元素
sort(sortedpick, NUM)

完整演示:读取每行学生成绩,排序输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function sort(ARRAY, ELEMENTS, temp, i, j) {
    for (i = 2; i <= ELEMENTS; ++i)
        for (j = i; ARRAY[j-1] > ARRAY[j]; --j) {
            temp = ARRAY[j]
            ARRAY[j] = ARRAY[j-1]
            ARRAY[j-1] = temp
        }
    return
}
{
    for(i=2;i<=NF;i++)
        grades[i-1] = $i
    sort(grades, NF-1)
    printf("%s: ",$1)
    for(j=1;j<=NF-1;j++)
        printf("%d ", grades[j])
    print ""
}

维护函数库

  • awk支持多个‑f参数,可以加载多个脚本文件。可以把公共自定义函数放到单独.awk库文件。
1
awk -f main.awk -f sort.awk data.txt

sort.awk只存放sort函数定义,main.awk是业务脚本。

另一个排序的例子(处理man手册Related Commands)

业务需求:提取.SH "Related Commands"后面多行的命令列表,去除标点,排序后重新输出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# sorter.awk
BEGIN { relcmds = 0 }
/\.SH "Related Commands"/ {
    print
    relcmds = 1
    next
}
(relcmds == 1) {
    commandList = commandList $0
}
(relcmds == 0) { print }
END {
    gsub(/, */, ",", commandList)
    gsub(/\. *$/, "", commandList)
    sizeOfArray = split(commandList, comArray, ",")
    sort(comArray, sizeOfArray)
    for(i=1;i<sizeOfArray;i++)
        printf("%s,\n", comArray[i])
    printf("%s.\n", comArray[i])
}

运行,需要同时加载sort函数库

1
awk -f sort.awk -f sorter.awk test.txt

第九章核心速记清单

  1. 算术函数
    • int()只截断小数,不四舍五入;三角函数参数是弧度。
    • rand()返回[0,1);不调用srand()每次运行随机序列固定;srand()用时间做种子。
    • 获取1‑N随机整数公式:1+int(rand()*N)。
  2. 字符串重要规则:awk字符串位置从1开始,不是0。
    • index(s,t)查找子串位置;substr(s,p,n)截取子串。
    • sub()只替换第一个;gsub()全局替换;替换侧&代表匹配内容,无//复用正则。
    • sprintf()生成格式化字符串,不直接打印,可以赋值变量。
    • match(s,r):匹配正则,设置RSTART起始位置、RLENGTH匹配长度。注意参数顺序,字符串在前,正则在后。
    • split()分割字符串到数组,返回元素个数。
    • tolower() / toupper()返回转换后的副本,原字符串不变。
  3. 自定义函数
    • 函数参数列表末尾放变量实现局部变量;函数内直接定义变量默认为全局。
    • 普通变量按值传递;数组按引用传递,函数内修改外部数组跟着变。
    • return返回函数结果。
  4. 函数库:多‑f选项加载多个awk脚本,把通用函数抽成独立库文件。