前言:
小编在学习生信的时候,awk总是不时出现在命令行操作当中,忍无可忍之后决心系统学习awk语法,结果发现awk其实是自成一套编程体系的(天,第一眼确实震惊我!),虽然有很多地方是有借鉴于C的,但是仍然有很多其独特语法所在之处。
本篇教程呢也仅仅是用来服务于生信学习的目的的,虽然对awk的各个方面都有介绍但是本着“深入理解但是够用就行”的学习理念(不喜勿喷),小编尽可能表述清楚我对awk的理解,对于很细节的option也不做深究(因为很可能用不上,要用再查也来得及),希望可以扫除生信应用场景当中的awk学习和理解障碍啦~
Workflow awk的工作流方式
在Linux当中,awk一般是作为一个文本处理工具出现的,所以一般来说给awk的输入是标准输入或者是以文件格式出现的,那么awk的执行的主体部分(后面有介绍),awk是一行一行读取然后操作的,这是awk作为文本处理工具的特点之一。
但是为什么说awk执行的主体部分呢?因为awk的实际的工作流方式是如下的:
+-----------------------+
| 启动 AWK 程序 |
+-----------------------+
↓
+-----------------------+
| 执行 BEGIN 块代码 |
| (可选,无则跳过) |
+-----------------------+
↓
+-----------------------+
| 读取输入流的一行 |
+-----------------------+
↓
+-----------------------+
| 对该行执行 AWK 指令 |
| (匹配模式、执行动作)|
+-----------------------+
↓
+-----------------------+
| 是否到达文件末尾? |
| ├─ 是 → 进入 END 块 |
| └─ 否 → 返回“读取行” |
+-----------------------+
↓
+-----------------------+
| 执行 END 块代码 |
| (可选,无则跳过) |
+-----------------------+
↓
| 程序结束 |
+-----------------------+
在开始会有一个BEGIN部分的程序,此程序只会在进入awk程序的开始被执行一次,同样的在最后也有一个END部分的程序,此部分程序只会在awk程序结束的时候执行一次,但是请注意这三个部分并不是必须同时存在的,甚至只有有其中任意一部分就可以正常执行。(在后面的例子可以看到很多时候甚至都没有主体部分)
那么小编再把这个工作流简单叙述一遍(可以对照plaintext来理解):首先会先执行BEGIN部分的代码,等到这部分代码结束之后呢,进入到主体部分的代码,这一部分的代码会逐行读取输入,读一行操作一行,然后再读下一行,直到读到最后一行(或者遇到特定指令如nextfile(后面介绍))进入到END部分程序,此部分执行结束之后awk的程序也宣告结束。但是注意一点:对于主体部分的代码可以由多个部分组成,后面提到的/pattern/{commands}只是基本单位而已哦,对于每一行输入流会依次从前到后执行每一块代码。
对于BEGIN,END,都遵循同一个格式:BEGIN{awk-commands}或END{awk-commands},{}里面的是awk的命令行,但是对于文本操作的主阵地BODY这一部分有一些不同(这里不需要BODY标识哦),/pattern/{awk-commands} 前面的pattern可以理解为条件(一般以正则表达式的形式出现),而{}里面的如同BEGIN和END部分是一样的均是操作(awk命令行),只有满足pattern的匹配格式,后面的commands才会在这一行执行
下面是一个简单的例子,具体讲解工作流
下面是一份示例文件(以空格分隔字段(简单理解就是列))
Alice Math 90
Bob English 85
Charlie Math 92
David English 78
Ella Math 88
awk '
BEGIN {
total = 0 # 总分
count = 0 # 科目为 Math 的行数
FS = " " # 字段分隔符设为空格
} #其实这一块并没有很大必要,因为awk当中如果没有定义total等变量直接使用的话默认值就是0,而且FS字段分隔符(输入文件数据的)也就是space
# Body 部分(带 pattern):匹配科目为 Math 的行
/Math/ {
print "匹配到 Math 科目行:" $0 # 输出当前行内容
total += $3 # 累加分数(这里的$3表示的是第三个字段)
count++ # 计数 +1
}
END {
avg = total / count
print "Math 科目共", count, "人"
print "平均分:", avg
}
' scores.txt #这里是输入数据
(格式为awk ‘BEGIN{commands} /pattern/ {commands} END{commands}’)
这里补充一下:除了我们上面介绍的这种格式,其实可以将‘ ’内的写成一个文件,以下面这种格式运行(当然了,在Linux当中一般是不需要这么复杂的,所以这里就不着重说了)
(格式为awk -f command.awk marks.txt)(其中command.awk就是上面编写的程序)
Built-in Variables 内置变量
我们先运行下面这一个程序然后逐一介绍里面重要的内置变量
awk --dump-variables ''
cat awkvars.out
秉持着理解深入的原则,我们还是先看看上面代码运行的逻辑是什么,首先呢--dump-variables的作用呢是导出(非显示!!!)这个awk程序当中的内置变量和自定义变量,比如说呢我在BEGIN程序里面定义了一个变量Count=1,那么Count就会被这个command导出。那我们注意到在其后是有一个‘ ’,其实这就是空脚本,可以理解为我不改动任何一个配置,所以最后导出的就是最原本的内置变量和默认配置。但是刚才其实也强调了,第一条语句只是把这些变量导出到这样一个文件awkvars.out当中,我们还是需要配合cat来显示
(小小广告,如果cat的具体用法您不清晰的话可以参考小编的另外一篇博客)Unix命令行入门-CSDN博客

我们从头开始,如果跳过说明就是不太重要的参量了
ARGC表示参数的个数,这里有1个(不是空脚本吗为啥有1个参量?是因为awk本身的shell脚本是字符串格式,它自己就占了一个参量)
ARGV是存储参量的数组(这里真的需要点赞awk的设计,我们在c,c++,Python的当中的数组(或列表)的索引只可以是数字但是在awk格式没有限制(甚至可以是字符串))
CONVFMT(conversion format)其规定了数字的转换格式就是%.6g(也就是数字默认的输出格式就是6位有效数字,以g的格式输出(如果是位数较少就采用一般计数,如果位数较多那么采用科学计数(后面有说明)))
FILENAME:顾名思义,表示的就是当前的文件名,那么这里就是‘’
FS (field separator)(字段分隔符)这里默认的字段分隔符就是单个空格,而且会忽略开头结尾的空格,并且将连续的空格视为一个分隔符(这点很好了),当然了可以在command当中更改的,一般是可以在BEGIN程序部分更改,对于分隔符的更改对于文本处理是一个非常有利且灵活的操作
NF(number of field)表示当前行(record)的字段个数
NR(number of record)表示当前行的编号(从1开始编号注意!)
OFS(output field separator)表示输出格式的字段分隔符,默认是单个space
ORS(output record separator) 表示输出格式行分隔符,默认是\n
RLENGTH (re-length) 表示被pattern匹配的字符串长度(字符为单位)
RS(record separator)表示输入格式的行分隔符,默认是\n
RSTART(re-start)表示被第一个匹配的字符的位置索引(从1开始)
$0直接代指整行,$n代指分隔符分隔之后的各个字段的编号(从1开始)
小小总结一下:其实对于filed separator和record separator无论是input还是output,都可以这么理解:只是我们如何区分和呈现字段与行的方式不同而已,比如我们可以认为我们的输入字段是以,分隔的,(FS=',')那么我们实际文本处理的时候就是会以这种方式去理解字段,同理行
Operators操作符
操作符的基本操作自我感觉是和Python大差不差的,有几个不太一样的小编单独拿出来说一下(或者是容易混淆的)
1. 首先是pre-increment和post-increment的不同之处(decrement请聪明的各位自行举一反三)
awk 'BEGIN { a = 10; b = ++a; printf "a = %d, b = %d\n", a, b }'
#output: a=11,b=11
awk 'BEGIN { a = 10; b = a++; printf "a = %d, b = %d\n", a, b }'
#output: a=11,b=10
其实就是注意到赋值的前后问题,第一个程序是先a加1然后赋值给b,所以a=b=11,第二个程序就是a先赋值给b然后a再++,所以a=11但是b=10(但是为什么要没苦硬吃呢,其实我们在写的时候直接分开来写就好了)
2. 逻辑和&&,逻辑或||,非!
3. condition expression ? statement1 : statement2 在Python当中也有类似语法,如果expression是对的,那么执行statement1,否则执行statement2(三目表达式)
4. 字符串的合并操作还是非常简单的,直接space连接即可
awk 'BEGIN { str1 = "Hello, "; str2 = "AWK"; str3 = str1 str2; print str3 }'
Regular Expressions 正则表达式
这里的正则表达式的应用可以说是文本处理的非常重要的一个环节,所以专门有一篇博客介绍
正则表达式入门-CSDN博客(也是小编自己写的,自己做个广告嘻嘻~)
在这基础上,简单注意一下我们的pattern需要用/regex/包裹的哦,除此之外,请看下面一个例子
awk '$1 ~ /apple/ || $3 ~ /apple/ { print }' file.txt
我们可以不限制字段(没有$n)此时我们也不需要写~,但是如果我们需要限制匹配字段的话就需要如上面所示的语法了(pattern使用也是很灵活的,可以配合逻辑表达式)
Array 数组
小编自以为数组是awk与其他编程语言不同之处,可以称得上是awk的highlight了(可能是因为小编见识过的编程语言不多所以敢如此放言),基本语法如下
array_name[index] = value
前面简单介绍过awk的index是可以取任何格式的,可以是正常的整数甚至可以是字符串(是不是很nice!),比如说如果我要生成这一个数组
100 200 300
400 500 600
700 800 900
如果按照Python的做法,得列表里面套列表对吧(或者用numpy多维数组括号不停套)
但是在awk,直接这么写就好了 array['1,1']=100,世界一下子晴朗了~
这里只是简单介绍一下基本语法
Control Flow 控制流
其实基本的控制流的关键if,else等还是没变的,这里就举一个例子就好,聪慧的大家自行体会(如果有编程基础的话,没有的话可能需要再查一下哦!)(当然也可以写在同一行)
awk 'BEGIN {
a = 30;
if (a==10)
print "a = 10";
else if (a == 20)
print "a = 20";
else if (a == 30)
print "a = 30";
else
print "a = 40";
}'
(习惯用Python的同学可能需要注意一下这里的分号表示不同语句哦还有这里是else if)
Loops 循环
对于循环和C语言其实几乎没有变动,但是为了严谨起见,咱们还是把各个循环语句过一遍
For循环
awk 'BEGIN { for (i=1;i<=5;++i) print i}'
For除了这一种方式控制,还可以 for i in a (print i) (那么这里的i其实遍历的是数组a的索引,如果要访问对应的Value的话直接a[i]即可)
While循环
awk' BEGIN
{ i=1;
while(i<6)
{ print i;
i++
}}'
Do-While Loop
awk 'BEGIN {
i=1
do {
print i
++i
}
while (i<6) }'
对于Break,continue就不再多说了,功能是一样的
对于exit表示退出程序,其后可以加一个constant如exit(10)来表示因为什么退出程序的
Built-in Functions 内建函数
这一块小编会尽可能讲的详细一点,可能在后面的文本处理当中会用到
1. 计算函数,这一块小编就简单过一遍哈~,因为基本一致
atan2(y,x)算arctangent(y/x)
cos(x)(弧度表示)
sin(x)(弧度表示)
exp(x)自然指数次幂
int(x)取整函数
log(x)自然对数为底对数
rand() 随机生成0-1内的数(左闭右开)
srand()设立种子值,保证可重复性
sqrt(x)开平方根
2. 字符串函数(字符串必须要用双引号包裹哦~)
(这里参数位置其实小编也没有记清楚,实际运用的时候可以再来查看)
asort 对数组的值排序
asort(arr [, d [, how] ]),[]表示可选,前面一个arr表示需要排序的数组,d(optional)表示排序之后存放值的数组,how表示希望按什么标准排序(这里可以用内建函数哦,这里不展开了)
这里强调一下有没有d的区别(强烈建议还是有的),如果不给目的数组的参数,那么最后asort的结果就会直接存放在原数组的位置,索引变成了排序序号,所以我们最初的索引信息没有了,但是如果给了目的数组,那么就把目的数组作为存放排序结果的地方,原来是数组就不会改变
asorti(arr [, d [, how] ])
这是对数组的索引进行排序,其他地方和asort相同略过
gsub(regex,sub,string)
全局替换,把一切满足regex(正则表达式)的字符串部分全部替换为sub(给定的字符串),string是我们操作的对象,比如
awk 'BEGIN { string="hello, awk"; gsub ("awk","World",string) print string}'
(注意直接在原字符串上更改)
index(string,sub)
判断子串存在的位置,如果sub是str的子串,那么返回sub第一个字符的索引,否则返回0
length(string)
返回字符串的长度
match(string,regex)
返回第一个匹配regex的最长子串的第一个字符索引,如果没有返回0
split(string,arr,regex)
将regex作为分隔符分割str将分割的字段储存在arr当中
(这里比一般的功能强大多了,因为有regex的加持,所以可以同时识别多个分隔符)
sub(regex,sub,string)
是gsub的缩版,仅仅更改第一个匹配regex的子串为sub
substr(string,start,length)
(其实有点类似Python的切片)是返回string子串的操作,从start索引开始(含)截取length个字符,如果不写length,那么直接提取到string的最后一个字符
tolower(string)
把string里面所有的字母全部改为小写
toupper(string)
把string里面所有的字母全部改为大写
3. 其他内建函数
close(x)
一般是用来关闭双管道的,请看下面一段程序(#后是对此行命令的解释,程序可能需要自己手打)(getline不懂的后面有详细介绍哦,请勿焦虑~)
awk 'BEGIN {
cmd = "tr [a-z] [A-Z]"
print "hello, world !!!" |& cmd
close(cmd, "to")
cmd |& getline out
print out;
close(cmd);
}'
#tr 是翻译的命令,此时相当于cd就是此翻译的命令字符串
#双管道有两个方向,一个方向是常规的数据写入命令,还有另外一个方向就是读得执行命令的结果,这里的结果必须需要getline来手动强迫换行读取
#第一个close用来关闭数据写入的通道,否则命令会等待继续输入
#用getline将执行cmd的结果储存在out文件内
#cmd是存储量Shell的一个命令字符串,双管道的操作必须要把命令转化为字符串格式(和C语言底层执行逻辑有关)
#关闭管道释放资源
delete arr[index]
删除数组元素
getline
直接读取下一行(如果没有getline对于一般body部分会自动读取,但是如果有getline,就会因此跳过第一行从第二行开始读取),在双管道当中读取命令的输出这一块是必备的
next
next一般式放在非循环程序中用来跳过本行剩余命令的(在循环当中,next的作用和continue一样)
nextfile
用来跳过本文件剩余命令的
system()
()内是某个command,system会试图执行这个command,然后将执行是否正常进行的结果返回(如果正常执行那么会返回0,如果没有正常执行就会返回非零值)
Getline 详细讲解
小编发现getline这个命令出现频率还是很高的,为了之后在Linux遇到可以不慌,特意整理了一些关于getline的必备知识点
其实getline最简单或者是最本质的理解就是对于输入流的读取,只是根据输入流来源的不同其的功能也有些许差别
1.首先是应用于一般无管道的command当中(后面会说)
echo '1\n2\n3\n4\n5\n' | awk 'BEGIN{getline} {print $0}'
echo '1\n2\n3\n4\n5\n' | awk '{getline} {print $0}'
咱们首先来看这两个程序(这里管道只是一般用法,不要在意)
第一个程序的getline是放在 BEGIN程序内部的,我们介绍过BEGIN是工作流的最开始,是在BODY 部分才会程序才会主动读取输入,所以BEGIN这里的getline读取的是第一行输入,但是并没有任何操作,那么接着进入主程序,由于第一行已经在BEGIN部分被getline读取,所以主程序从第二行开始执行,最后打印结果是2 3 4 5(换行符省略)
我们再来看第二个程序和第一个程序之间有什么不同,这里的getline是放在了主程序里面。我们一进入主程序,程序已经读取了第一行输入(这里还没执行getline),然后呢我们立马就执行了getline,我们读到了第二行,第一行的数据被我们覆盖了,那么第一次print的其实是第二行的结果2,然后再回到主程序开头(因为输入还没读到最后一行),主程序读到了第三行,但是一旦我们执行了getline,第三行数据有被第四行数据覆盖,所以我们打印的是4,最后我们主程序读到第五行,但是无脑的getline继续向下读,结果是空行,所以最后没有打印。总而言之结果就是2 4
我们初步了解之后,我们可以总结:主程序和getline是共同控制读取到第几行的,主程序是在一开始进入即读(可以理解为在{处开始),而getline是需要执行这个命令开始读
在明白了这个道理之后,我们看到一个稍微加点料的例子
echo '1\n2\n3\n4\n5\n' | awk '{getline next_line} {print $0}'
稍稍变化一下,在getline后面加上一个参数来接收此时getline读到的行,结果会有什么不同吗?
运行一下发现,结果是1 3 5,为啥呢?
原来给getline加上一个参量就可以使得把getline读到的该行存储到next_line当中,从而不覆盖主程序读取的$n的数据,所以$0表示的仍然是主程序读到的,而在之前的例子当中,getline后面没有参量,那么getline就会将自己读到的行覆盖主程序的$n信息,所以输出的是getline该行的$n。
但是仍然要注意一点,我们只讲到覆盖的问题,getline仍然是控制了程序读到哪里了,下一次主程序读取的仍然是getline读取的下一行(尤其注意哦~)
2. 好了,我们现在再来看有管道的getline是如何使用的,我们先暂且借用下内建函数当中close的例子
awk 'BEGIN {
cmd = "tr [a-z] [A-Z]"
print "hello, world !!!" |& cmd
close(cmd, "to")
cmd |& getline out
print out;
close(cmd);
}'
我们之前说到close在这里是起到关闭管道的作用以释放资源,我们在这里再深入了解一下
首先cmd是命令的字符串,我们将"hello, world !!!"的输入传给cmd这个命令,然后cmd的输入就可以终止了,所以我们close(cmd,'to')这一步我们只关闭输入通道,但是对于cmd的输出通道我们还是没有关闭的
在cmd |& getline out这里,其实是将cmd执行的结果(理解为水)全部放在管道(理解为水龙头)当中,此时cmd由于输入结束,所以不会再产生任何的水了,产生的所有的的水就全部放在了管道当中(作为进一步处理的对象,所以可以当成是另一级输入)。但是程序不会主动地从管道中获取输入,所以必须要借助getline,得到管道当中的‘水’,并将其存储在out当中。在这一步结束之后,管道中的水已经没了,所以我们可以关闭管道(如果不关闭,管道自身已经没有输送功能,但是也不可以给其他子进程使用所以会浪费资源)。但是需要非常注意的一点是,如果cmd产生的是多行输出怎么办,也就是管道当中的水我们用一次getline运不完,这个时候我们就需要用到循环来操作
awk 'BEGIN {
cmd = "tr [a-z] [A-Z]"
print "hello, world !!!" |& cmd
close(cmd, "to")
while( cmd |& getline out)
{print out}
close(cmd);
}'
那么这里我们不断用循环读取管道当中的内容,然后等到读取结束之后,关闭管道(注意必须是循环结束之后关闭,因为如果在循环内部关闭,那么管道内部还有水但是没有获取)
getline的理解就讲到这里,只是希望之后见到getline的时候可以理解其的作用,然后慢慢模仿写法
User Defined Functions 自定义函数
自定义函数的方式和Python是一致的,可以仿照,这里不赘述了(当然了仅仅是如何定义和调用是一致的,对于函数里面的内容就参考本博客其他内容了)
Pretty Printing 打印
打印这一块基本上是和C语言是一致的,做一个简要介绍
print可以用于简单打印,比如直接打印一个string之类的,但是涉及到格式化打印还是要交给printf,下面也即是着重介绍printf的打印
基本语法: printf format variable-list
| \n | 换行符 |
| \t | 水平制表符 |
| \v | 垂直制表符 |
| \b | 将光标移至前一个字符 |
| \r | 将光标移至此行开头 |
简单例子如下:
awk 'BEGIN { printf "HAHAHA 1\b HAHAHA 2\bHAHAHA 3\bHAHAHA 4\n" }'
#输出 HAHAHA HAHAHA HAHAHA HAHAHA 4
和C一致,printf打印需要格式说明符(Format Specifier)
| %c | 单个字符(遇到数字那么就是认为是单个字符对应的ASCII值) |
| %s | 字符串 |
| %d | 整数 |
| %f | 浮点数,如以86.2300呈现 |
| %e | 科学计数法,如以8.62300E+01 |
| %g | 根据数的大小和位数选择是f还是e表示 |
| %o | 八进制,如printf‘%o’,10输出12 |
| %x | 十六进制,如printf‘%x’,10输出A |
| %u | 转变为无符号数 |
对于要表示%本来的字符,需要两次转义%%
输出的选择参数(宽度和对齐格式)
Width:对于宽度的调控是将Width放在格式说明符%和。。之间
如printf 'Num=%10d' 即右对齐10字符宽度以space填充
(如果想要用0填充,那么即写printf 'Num=%010d' )
默认是右对齐,如果想要左对齐,那么用-
printf 'Num=%-10d'即左对齐10宽度
注意:如果是负号也会占一个字符哦~
总结
本篇只是简单系统性介绍了awk的基本语法,可以当成是一个introduction类型的内容,内容覆盖Linux生信基本用法应该是没有问题滴,但是在Linux熟练掌握awk还需要不断练习和学习呀~
小编水平有限,如果错误不足之处请多指教友好交流哦~
&spm=1001.2101.3001.5002&articleId=149782937&d=1&t=3&u=eeb873c7262b49018ae011e184d5ae37)
11万+

被折叠的 条评论
为什么被折叠?



