本帖最后由 丁越 于 2024-6-12 21:39 编辑
shell 编程—awk语法小结
一、前言
这两天遇到一个给原子添加后缀的需求,比如把“C”后面添加"_1"的后缀为"C_1",以便区分体系不同位置的C原子然后为其赋予不同类型的基组。起初利用简单的shell语法虽然能实现这一需求,但是总觉得可以用awk编程做一些改进,使其变得更为精简。于是借着写脚本之余把awk编程的一些基本语法回顾了一下,在此做个小总结以方便查阅使用一些基本语法。更复杂的awk编程请出门左转向"shell小王子" 钟叔(ggdh)拜师学艺(俺就不献丑啦)。
二、基础用法
awk基本语法结构如下所示:
awk [选项参数] 'pattern1 {action1} pattern2 {action2}…' file
awk后面接两个单引号并加上大括号括来设置对数据进行的操作,file是所要处理的文本文件,但awk也可以接受前面命令的标准输出。awk主要是处理每一行 (也叫每一条记录) 的字段内的数据,默认的字段分隔符是"tab"或者"空格"。
常用的选项参数:
-F: 指定文件内容的分隔符,等价语法可以用BEGIN{FS="分隔符"},但是一般情况下前者使用更加方便所以推荐使用。
-v: 设置用户自定义变量,但常用于读取外部变量到awk中。比如自定义变量 awk -v num=1 'BEGIN{print num}',这里自定义了num变量值为1,注意awk中的内部变量不需要用“$”符号引用,其次字符串内容一定要用" "括起来,否则会误把字符串当作变量处理。
再比如读取外部变量:awk -v myvar="${global_var}" '{action}' file。假如有多个变量要读取那就写多次-v myvar="${global_var}"。
常用的pattern:
pattern的内容可以是以下这些:
* 正则表达式,其内容要放在两个斜杠之间,比如"/^[0-9]+/"。
* $n,表示所处理的当前行中的第几个字段, 其中$0表示整个所处理的当前行。
* 一些运算符,比如逻辑运算符与("&&"),或("||"), 非("!");判断正则表达式匹配和不匹配的"~"和"!~";关系运算符"<, >, >=, !=, =="。
* BEGIN{ } 和END{ }分别表示处理file之前和之后所要执行的动作
下面看几个简单的小例子来帮助理解。假如现在有一个名为test.dat文件,其文件内容如下:
- Alpah 30 50 90
- beta 35 55 88
- Gamma 55 88 100
- tHeTa ww 44 u8
复制代码例1:我需要找出第一个字段是“Beta”的这一行 - awk '$1=="Beta"' test.dat
复制代码输出结果是:beta 35 55 88
例2:我需要找出第一个字段中名字全部是小写字母的一行 - awk '$1 ~ /^[a-z]+$/' test.dat
复制代码输出结果是:beta 35 55 88 其中,正则表达式中“^”表示开头,“$”表示末尾,“+”至少匹配一次,”[ ]”表示匹配方括号里面字符之一
例3:我需要找到第三个字段中值大于50的所有行 输出结果是:beta 35 55 88 Gamma 55 88 100
例4:输出整个文件内容,但在处理test.dat文件前用BEGIN 打印“File processing is beginning..”, 处理完后打印“All work has been done~” 结果如下所示: - awk 'BEGIN{print "File processing is beginning.."}{print}END{print "All work has been done~"}' test.dat
复制代码File processing is beginning.. Alpah 30 50 90 beta 35 55 88 Gamma 55 88 100 tHeTa ww 44 u8 All work has been done~
常用的内置变量: $n: 表示第几个字段。 其中$0表示整个所处理的当前行。 FILENAME: 当前文件名。 NR: 当前awk已经处理过的记录数,也就是处理到第几行,默认从1开始。假如有多个文件(awk后面可以接多个file),那么这个处理过的记录数就会一直累积直至最后一个文件的最后一行。 FNR: 各文件分别计数的已经处理过的记录数(行号,从1开始)。也就是上面NR提到的当有多个文件时,每次开始处理新的文件FNR的记录数就会从1重新开始计数。 NF: 每一行的字段数目,也就是被分隔符隔开后得到的列的数目。 FS: 字段分隔符,默认空格,支持正则表达式。 RS:记录分隔符,默认是“\n”。 OFS、ORS:这两个分别控制输出字段和记录的分隔符,但是一般用处不大,习惯上用print或者printf控制输出格式。
了解了上述这些后,下面为刚才的test.dat文件各字段内容添加标签“name math writing game”,并且只输出前三行的内容,最后打印各个字段的均值: - awk 'BEGIN{printf "%s\t%s\t%s\t%s\n","name","math","writing","game"}NR<4{m2+=$2;m3+=$3;m4+=$4;print}END{printf "%s\t%d\t%d\t%d\n","Mean",m2/NR,m3/NR,m4/NR}'test.dat
复制代码得到下面结果: name math writing game Alpah 30 50 90 beta 35 55 88 Gamma 55 88 100 Mean 24 39 56
常用的函数: * 数学函数 sin(x)、cos(x)、atan2(y,x)、exp(x)、log(x)(返回x的自然对数)、sqrt(x) (返回x的平方根)、int(x) (将x取整) * 字符串操作函数 sub(regexp, replacement [,string]): 将正则表达式”regexp”匹配的字串用”replacement”替换,注意只替换一次。第三个参数”String”是可缺省参数,默认是$0。 gsub(regexp, replacement [,string]):含义同sub,但区别是会将匹配的所有字串全部替换。 例如: - awk 'BEGIN{var="The result is wrong!";sub("wrong", "right", var);print var}'
复制代码输出:The result is right!
substr(string, start [, length]):返回string中第start个字符开始,长度为length的子字串。当长度参数缺省时是start开始一直到最后一个字符的子字串。 例如: - awk 'BEGIN{var="The result is wrong!";out=substr(var,2,5);print out}'
复制代码输出:he re
asort(source)、asorti(source):这两个都是对数组进行排序的操作函数。区别在于前者对数组值进行排序,排序后会丢掉原始索引。后者仅对数组索引进行排序,但是与数组值之间的映射关系会改变。 例如: - awk'BEGIN{a["first"]="d"; a["second"]="b";a["third"]="c"; asort(a);for (i in a) print i, a[i]}'
复制代码输出: 1 b 2 c 3 d
- awk 'BEGIN{a[3]="a"; a[1]="b";a[2]="c"; asort(a); for (i in a) print i, a[i]}'
复制代码输出: 1 a 2 b 3 c
index(in, find):查找并返回字符串 in中find字符第一次出现的位置, 从1开始编号。 例如: - awk 'BEGIN{print index("apple","pp")}'
复制代码输出:2
length([string]):返回字符串中字符的数目。来看下面三个例子加深理解: - awk 'BEGIN{print length("aa")}'
复制代码输出:2 ,这个好理解,因为“aa”中一共两个a字符。 - awk 'BEGIN{print length("3 *2")}'
复制代码输出:5 - awk 'BEGIN{print length(3 * 2)}'
复制代码输出:1 咦,后两个是怎么回事呢?注意前者3 * 2被打上了双引号,因此它表示这样的一个字符串,三个非空字符加上两个空字符一共5个字符。最后一个它实际上进行了数学运算,得到的数字6然后被转化成了字符“6”,所以就1个字符啦。
match(string, regexp):在字符串中搜索与正则表达式 regexp 匹配的最长、最左侧的子字符串,并返回该子字符串开始的字符位置(索引)(从1开始编号)。如果未找到匹配项,则返回零。 例如: - awk 'BEGIN{str="hTdTdTdww";print match(str,"Td")}'
复制代码输出:2
split(string, array, [seps]):将 String 参数指定的参数分割为数组元素 A[1], A[2], . ..,A[n],并返回 n 变量的值。此分隔可以通过 seps 参数指定的扩展正则表达式进行,或用当前字段分隔符来进行。 例如: - awk 'BEGIN{str="a,b,c,d"; n=split(str,a,","); for (i in a) print i, a[i]; printf "string has been split into %d pieces\n", n}'
复制代码输出: 1 a 2 b 3 c 4 d string has been split into 4 pieces
tolower(string):返回字符串的副本,字符串中的每个大写字符替换为其相应的小写字符。非字母字符保持不变。 toupper(string):类似tolower(string),小写字符替换为大写字符。
常用的action: * print和printf:二者都是打印输出,区别在于后者可以格式化输出,并且print输出字串后面自动包含“\n”换行符,而对于printf则没有。 %d: 打印十进制整数。例如awk 'BEGIN{printf "%5d","33"}' %f:打印浮点数。例如 awk'BEGIN{printf "%-5.3f", "33"}' %e: 以科学计数法形式打印数字。例如awk 'BEGIN{printf "%.2e", "33"}' %s: 打印字符串。例如 awk'BEGIN{printf "%3s", "abcd"}' 上面中比如"%-5.3f"的含义是:共5个字符长度,小数部分占3个,并且左对齐(不加"-"默认右对齐)
* 条件控制 1. If语句: if (condition) { action } #如果condition为真,则执行action
2. if-else语句: if (condition) { action } else { action} #如果condition为真,则执行第一个action,否则执行第二个action
例如:打印第一个字段大于10的行awk '{if($1 > 10) {print $1}}' file 在编程时为了体现书写格式的优美以及层次性,我们可以将if-else语句如下书写: - awk '{if ($1 > 10) {
- Print "it is"
- } else {
- Print "it’s not"
- }
- }' file
复制代码这样一来我们可以变相实现if-elif-else这样的多条件判断语句,因为像“elif”在awk中是不支持的: - awk 'BEGIN {
- a=30
- if (a==10) {
- print "a = 10"
- } else if (a == 20) {
- print "a = 20"
- } else if (a == 30) {
- print "a = 30"
- }
- }'
复制代码此外,假如“{ }”中的action就只有一个时,花括号可以不用写,只需要在action后面写上“;”即可,如下所示,避免了大括号的数量一多时造成眼花缭乱导致看错括号层级。还有注意在“{ }”内要是同一行内存在多个action时,每个action之间需要用“;”隔开。 - awk 'BEGIN {
- a=30
- if (a==10)
- print "a = 10";
- else if (a == 20)
- print "a = 20";
- else if (a == 30)
- print "a = 30";
- }'
复制代码3. C条件表达式(?:)是实现if-else的简洁途径,案例如下: - awk '{max=($1 > $2) ? $1 : $2; print max}' file
复制代码如果$1 > $2 成立,则把$1赋值给max,否则把$2赋值给max。
* while (condition) { action }: 只要condition为真,则一直执行action 例如:awk '{while($1 > 10) {print; $1--}}'file: 打印第一个字段大于10的行,并将第一个字段减1,直到第一个字段小于或等于10。 再比如while循环判断奇偶: - awk 'BEGIN{
- num=10
- while (num > 0) {
- if (num%2 == 0)
- printf "%s is even\n",num;
- else
- printf "%s is odd\n", num;
- num--
- }
- }'
复制代码* for (variable=start; condition;increment) { action }: 从start开始循环,只要condition为真,则执行action,然后执行increment操作 - awk '{for (i=1; i<=NF; i++) {print $i}}' file # 打印每个字段
复制代码* break: 跳出当前循环 * continue: 跳过当前循环的剩余部分,进入下一次循环 * next:强制awk停止处理当前记录并进入下一条记录的处理 例如: - awk '{if ($1 ~ /^[A-Z]/) next; print}' test.dat
复制代码输出: beta 35 55 88 tHeTa ww 44 u8 上式的含义是第一个字段假如匹配到带大写字母开头的字串那么就会跳过该记录,然后只输出第一个字段开头字母全为小写字母的记录。
awk中的数组: 1. 定义数组,格式例如a[索引]="value"。 awk的数组在使用时不需要提前对数组进行声明,所以比较灵活。数组索引可以为整数、小数甚至字符串,如果是字符串,要在字符串上添加双引号。注意只支持1维数组,但通过巧用索引书写规则可以模拟多维数组,比如a[1,1] ="11";a[0,1] = "01"。 - awk 'BEGIN{a[1]="x"; a[2]="y"; a[3]="z"; print a[1], a[2], a[3]}'
复制代码2. 判断数组中的索引成员是否存在 - awk 'BEGIN{a[1]="x";a[2]="y"; a[3]="z"; if (1 in a) print "In"; else print "Not in"}'
复制代码其中if (1 in a)中判断的是a的索引中是否包含1,而不是a的数组值value是否含有1,这一点和python是有很大区别的。 3. 遍历数组 - 1)awk 'BEGIN{a[1]="x"; a[2]="y"; a[3]="z"; for (i in a) print i,a[i]}’ #推荐使用
- 2)awk 'BEGIN{a[1]="x";a[2]="y"; a[3]="z"; for (i=1; i<=length(a);i++) print i,a[i]}'
复制代码4. 删除数组中元素 - awk 'BEGIN{a[1]="x";a[2]="y"; a[3]="z"; delete a[2]; for (i in a) print i,a[i]}'
复制代码
一些常用语法: 1. 提取cp2k的restart文件中原子速度 - awk '/&VELOCITY/, /&END VELOCITY/{if (!/&VELOCITY/ && !/&END VELOCITY/) print}' test-1.restart
复制代码2. awk中字符串拼接 - echo "A" | awk '{b=$1"_1"; print b}'
复制代码3. 查询第一列的最大值(最小也类似) - awk 'BEGIN{max=0}{if ($1 > max) max=$1}END{print max}' test.dat
复制代码4. 对某列内容求和 - awk 'BEGIN{sum=0}{if ($1 != "#!") sum+=$1}END{print sum}' test.dat #if的作用是开头的注释行不会被纳入计算
- 当然有时可以直接写为:awk '{sum+=$1}END{print sum}' test.dat
复制代码5. awk中可将变量放在后面 - awk '{if($1!="#!") print $2, exp(($4-bmax)/kbt)}' kbt=2.494339 bmax=23 file.dat
复制代码6. 把每列相加,然后在最后打印每列的总和 - awk '{for(i=1; i<=NF; i++) sum[i] += $i}END {for(i=1; i<=NF; i++) print sum[i]}' file
复制代码7. 打印第一个字段匹配正则表达式的行 - awk '$1 ~ /regex/{print}' file
复制代码8. 把每个字段中所有匹配的"old"替换为"new",然后打印当前行 - awk '{gsub(/old/, "new", $0);print}' file
复制代码9. 打印内容及其行号 - awk '{print NR, $0}' file
复制代码10. 打印每个字段 - awk '{for(i=1;i<=NF;i++) {print $i}}' file
复制代码11. 以逗号为分隔符,打印第二个字段 - awk -F ',' '{print $2}' file
复制代码
11. 按行合并两个文件
- awk 'NR==FNR{a[FNR]=$0;next}{print a[FNR],"\t",$0}' file1 file2 > merged.dat
复制代码
综合案例-给xyz格式文件中原子添加后缀: - #!/bin/bash
- function help() {
- echo "Usage: atm_suffix.sh [.xyz format file]"
- }
- if [[ $# -lt 1 ]]; then
- help; exit 1
- fi
- echo 'Input atom indices, e.g. 2,3,5-9'
- read num
- if [[ -z ${num} ]]; then
- help ;exit 1
- fi
- echo "Input symbol to add the suffix, default '_1' by press enter:"
- read symbol
- if [[ -z ${symbol} ]]; then
- symbol="_1"
- fi
- awk -v N="${num}" -v sym="${symbol}" '
- BEGIN{
- split(N,a,",")
- for (i in a) {
- if (a[i] ~ /^[0-9]+$/) {
- b[a[i]] = 1
- } else {
- split(a[i],num,"-")
- for (j=num[1];j<=num[2];j++) {
- b[j] = 1
- }
- }
- }
- }
- {
- if (NR == 1) {
- print $1 >> "coord_new.xyz"
- } else if (NR ==2) {
- print "" >> "coord_new.xyz"
- } else if ((NR-2) in b) {
- sub($1,$1sym)
- printf "%-5s %18.6f %18.6f %18.6f\n", $1,$2,$3,$4 >> "coord_new.xyz"
- } else {
- printf "%-5s %18.6f %18.6f %18.6f\n", $1,$2,$3,$4 >> "coord_new.xyz"
- }
- }' $1
- echo "Done~"
复制代码三、 总结 上面只是简单的总结了一下awk的一些基本语法,也是我平常所使用到的一些语法结构。如果大家觉得平常用的比较多但帖子中有没有具体写到的一些语法,欢迎在下方评论区展示出来,我将及时地更新到本贴内容当中。如果是第一次学awk语法感到很困惑、费劲的话,不要因这点难度放弃了这个好工具,因为你一旦熟悉这些语法结构,就会体验到当面对一些文件处理时awk将会展现出多么强大的功能性和灵活性。所以平常将常遇到的语法刻意记一记,多用上几次慢慢就会了。
|