GNU AWK

详情请参考The GNU Awk User’s Guide

AWK得名于三位创始人Alfred V. Aho、Peter J. Weinberger 和 Brian W. Kernighan姓的首字母,有AWK、NAWK (new AWK)、GAWK (GNU AWK)三个版本,我们这里用的是GAWK。

AWK的编程模式很简单:找到一个模式然后执行一个动作(find a pattern in the input and then perform an action)。使用AWK可以很方便地对文本的行列进行处理并格式化输出,AWK还能进行数字运算。AWK将文件分成一个个的记录(Records),又把记录拆分成一个个的字段(Fields),默认是一行一个记录,一个单词一个字段,可以更改分隔符自定义记录和字段。

本教程使用说明

这里并没有展示示例代码的输出,需要学习者运行代码。 运行代码并理解为何会得到相应结果。 给出的示例代码都是可以直接在Linux Shell运行的。

AWK 初步了解

在linux系统下,使用man awk命令即能看到使用手册:

GAWK(1)                                Utility Commands                                GAWK(1)

NAME
       gawk - pattern scanning and processing language

SYNOPSIS
       gawk [ POSIX or GNU style options ] -f program-file [ -- ] file ...
       gawk [ POSIX or GNU style options ] [ -- ] program-text file ...

       pgawk [ POSIX or GNU style options ] -f program-file [ -- ] file ...
       pgawk [ POSIX or GNU style options ] [ -- ] program-text file ...

       dgawk [ POSIX or GNU style options ] -f program-file [ -- ] file ...

DESCRIPTION
       Gawk  is the GNU Project's implementation of the AWK programming language.  It conforms
       to the definition of the language in the POSIX 1003.1 Standard.  This version  in  turn
       is  based  on  the  description in The AWK Programming Language, by Aho, Kernighan, and
       Weinberger.  Gawk provides the additional features found in the current version of UNIX
       awk and a number of GNU-specific extensions.

       The  command line consists of options to gawk itself, the AWK program text (if not sup‐
       plied via the -f or --file options), and values to be made available in  the  ARGC  and
       ARGV pre-defined AWK variables.

       Pgawk  is  the profiling version of gawk.  It is identical in every way to gawk, except
       that programs run more slowly, and it automatically produces an  execution  profile  in
       the file awkprof.out when done.  See the --profile option, below.

       Dgawk  is  an  awk  debugger. Instead of running the program directly, it loads the AWK
       source code and then prompts for debugging commands.  Unlike gawk and pgawk, dgawk only
       processes  AWK  program source provided with the -f option.  The debugger is documented
       in GAWK: Effective AWK Programming.

这里可以看到pgawk、dgawk,另外还有个igawk,可以使用man igawk查看。用ls -l /bin/*awk可以看到相应的文件:

lrwxrwxrwx 1 root root      4 Apr  2  2018 /bin/awk -> gawk
-rwxr-xr-x 1 root root 514168 Jun 29  2017 /bin/dgawk
-rwxr-xr-x 1 root root 428584 Jun 29  2017 /bin/gawk
-rwxr-xr-x 1 root root   3188 Jun 29  2017 /bin/igawk
-rwxr-xr-x 1 root root 428672 Jun 29  2017 /bin/pgawk

awk是gawk的软连接,直接使用awk就等同于使用gawk,比如试试:

awk 'BEGIN { print "OK" }'

简单运用示例

可以先看看这些简单的运用示例,对AWK有个初步的印象。
这里以创建一个示例文件:

echo -e "Chr\tNum\nA\t65\n\t\n\t\nB\t66\n\n\nC\t67\n \t \n \t \nX\t88\n   \n   \nc\t99\nC\t67\nC\t67\nc\t99" > test.txt

第3、4行只有一个制表符,第6、7行为空行,第9、10行为一个制表符分隔两个空格,第12、13行各为三个空格。

test.txt
Chr	Num
A	65
 
 
B	66
 
 
C	67
 
 
X	88
 
 
c	99
C	67
c	99
C	67

打印出一个文件的所有行

只设置pattern的时候默认print $0

awk '1' test.txt

print默认print $0,有action而不设置pattern的时候默认pattern匹配所有行:

awk '{print}' test.txt

如下这样不指定pattern也不指定action是不行的:

awk '{}' test.txt

给各行加上行号

awk '{print NR, $0}' test.txt

提取含有特定字符串的行

awk '/C/' test.txt
awk '/6/' test.txt

去除标题行

awk 'NR > 1' test.txt
awk 'NR > 1 {print}' test.txt

打印特定的行

awk 'NR >= 2 && NR <= 4' test.txt

去除只有空格的行

NF是字段的数量,对于空行,NF是0,对于只有空格、制表符等情况的行这个NF也是0,在如下情况下不会被输出:

awk 'NF' test.txt

当指定了分隔符为制表符(\t)时,就只有空行被去掉了:

awk -v FS="\t" '{print NF}' test.txt
awk -v FS=" " '{print NF}' test.txt

去除空行

空行是除了换行符以外没有别的字符的行,如果字段分隔符被设置为“”,那么每个字符单独作为一个字段,可以看看每行的字段数:

awk -v FS="" 'NF' test.txt
awk -v FS="" '{print NF}' test.txt

输出特定的列

输出字段分隔符默认是空格:

awk '{print $1,$2,$1,$1}' test.txt

如果要输出列的分隔符为制表符,需要特别指定:

awk -v OFS="\t" '{print $1,$2,$1,$1}' test.txt

FS和OFS也可以在BEGIN里设置

awk 'BEGIN{FS="\t"; OFS=","} {print $1,$2,$1,$1}' test.txt

计算列的加和

awk '{SUM+=$2} END{print SUM}' test.txt

根据一列求另一列的和

比如这里根据第一列求第二列的和,就是把第一列相同的行的第二列相加:

awk '{X[$1]+=$2} END{for(i in X) print i, X[i]}' test.txt

如果按第一列原有的顺序,可以这样:

awk 'NR==FNR {X[$1]+=$2; next} {N[$1] += 1; if(N[$1] == 1) print $1, X[$1]}' test.txt test.txt

计算行数

awk '{SUM+=1} END{print SUM}' test.txt

.表示任意字符,如下表示至少要含有一个字符,计算非空行的行数,有空格和制表符的不是空行:

awk '/./ {SUM+=1} END{print SUM}' test.txt

查看与去除重复行

如下命令,每一行在执行的时候,放入数组,加上1,当第二次及以后出现的时候A[$0]值大于0了,就被输出了。A[$0]++是先用这个值,然后再+1。

awk 'A[$0]++' test.txt

可以对于超过一次的行输出一次:

awk 'A[$0]++ == 1' test.txt

++A[$0]是先+1,然后再用这个值:

awk '++A[$0] == 2' test.txt

因此,去除重复的行(对于有重复的行只保留一次):

awk '++A[$0] == 1' test.txt

如果对于存在重复的行都不输入,那么就要等都运行完了之后再决定怎么输出:

awk 'NR == FNR {++A[$0]; next} A[$0] == 1 {print}' test.txt test.txt

AWK 命令语法

文件输入

awk [options] 'pattern {action(s)}'  input-file(s)

最后的input-file(s)是输入文件,可以是多个文件(空格隔开)。命令部分用单引号 ' ' 引起来。

默认输出来stdout,可以用输出操作符>或»写到或者附加到一个文件:

awk [options] 'pattern {action(s)}'  input-file(s) > output-file

管道输入

如果不给定输入文件,默认为标准输入, 通过管道传入,比如:

cat input-file(s) | awk [options] 'pattern {action(s)}' > output-file

试试如下执行之后管道输入1,得到结果

echo -e "1\n2\n3" | awk '{print $1*$1}'

键盘输入

另外,可以键盘输入,输入如下命令回车之后等待键盘输入,输入一个数字之后回车就看到数字的平方根。需要有用Ctrl+C退出程序

awk '{print sqrt($1)}'

使用AWK脚本

可以把AWK命令写到一个文件,通过-f选项调用。

awk -f script-file input-file(s)
echo "/W/ {print} /o/ {print}" > test-script.awk      # 写一个测试脚本文件
echo -e "Wang\nHong\nWhong"    > test-input.txt       # 一个测试输入文件
awk -f test-script.awk test-input.txt

AWK命令执行顺序

Gawk以如下顺序执行程序:

  1. 先根据-v设置的参数,给变量赋值
  2. 将程序编译成内部的形式
  3. 如果有BEGIN模块,则先执行性BEGIN模块
  4. 然后开始读文件,如果没有给的文件则从标准输入读取,即从管道读取或者键盘输入
  5. 对输入中的记录逐一判断是否匹配某个pattern,匹配则执行相应的action。有多个patterns则按程序中的顺序逐个匹配
  6. 最后,当输入中的每条记录都被执行完,则执行END模块

options

[options]里边主要用到的有-v、-f:
-v在程序运行前设置变量,可以设置很多个,每个变量一个-v
-f后边接预先编好的AWK代码,同样可以有多个
不设置-v时,一些内置的变量采用默认值,比如,记录分隔符RS默认是换行符\n,字段分隔符FS默认是空格和/或制表符,对应的,有输出时的记录分隔符ORS和字段分隔符OFS。

ls -l /bin/ | awk -v OFS=","  -v ORS="\t" '/awk/ {print $1,$2,$3}'

pattern

pattern可以是正则表达式、条件表达式等等的。而当有action而无pattern的时候,默认的pattern就是匹配所有的

ls -l /bin/ | awk '{print}'

正则表达式

  1. 第一列含有l的输出来:
    ls -l /bin/ | awk '$1 ~ /l/'

条件表达式

  1. 第二列等于2的输出来
    ls -l /bin/ | awk '$2==2'
  2. 第5列大于1000000的输出来
    ls -l /bin/ | awk '$5 > 1000000'

BEGIN与END

BEGIN和END是两个特殊的pattern。如果有BEGIN{action(s)},则最开始执行的,然后才是再对每个记录执行其余的pattern-actions

ls -l /bin/ | awk 'BEGIN{print "All awk found:"} /awk/ {print}'

如果有END{action(s)},则对每个记录执行其余的pattern-actions后再执行END{action(s)}

ls -l /bin/ | awk '/awk/ {print; i+=1}END{print "Total AWK Count: "i}'

action

当有pattern而没有action的时候,默认action就是print,当不指定print的对象的时候,就是把所有的print出来

ls -l /bin/ | awk '/awk/'

可以有多个pattern-actions,则依次写。AWK会对每个Record都依次进行pattern的判断以及(当pattern满足时)之后后边的action。同一个action有多个命令时用花括号括起来,命令之间以半角分号(;)分隔。 如果执行完一个action之后对当前Record不再执行后边的,则用next转到下一条Record。

ls -l /bin/ | awk '/awk/ {print}'
ls -l /bin/ | awk '/awk/ {print} /gwak/ {print}'                       # 同时满足这两个条件的被print了两次
ls -l /bin/ | awk '/awk/ {print; next} /gawk/ {print}'                 # 匹配awk之后转入下一条,尽管同时匹配gawk,并没有被再次print
ls -l /bin/ | awk '/awk/ {print; next} /gawk/ {print} /bash/ {print}'

记录(Record)与字段(Field)

AWK将文本分为多个记录(Record),逐个记录进行处理。默认情况下,记录之间以换行符(\n)分隔,因此默认是逐行处理。可以自定义记录的分隔符(RS),就不局限于逐行处理了,可以把多行当作一个Record。

每个记录又分为多个字段(field),默认情况下,字段之间的分隔符(FS)为空白字符,包括空格、制表符(\t)、换行符(当RS不是换行符时,字段中的换行符也会被当做是FS)。如果自定义FS为换行符,那么每行当作一个field。 $0表示整个Record,$1表示当前Record的第一个field, $n表示第n个field. NR是当前记录的序号。 NF表示当前记录的字段数,因此$NF则表示当前记录的最后一个字段。 字段分隔符可以用-F选项设置。
输出时的字段分隔符(OFS)默认是空格,输出时的记录分隔符(ORS)默认是换行符(“\n”)。

echo -e "A:a\t1\nB:b\t2\nC:c\t3" 
echo -e "A:a\t1\nB:b\t2\nC:c\t3" | awk '{print $1}'            
echo -e "A:a\t1\nB:b\t2\nC:c\t3" | awk '{print $1,$2}'            
echo -e "A:a\t1\nB:b\t2\nC:c\t3" | awk -v OFS="," '{print $1,$2}'            
echo -e "A:a\t1\nB:b\t2\nC:c\t3" | awk -v OFS="\n" '{print $1,$2}'            
echo -e "A:a\t1\nB:b\t2\nC:c\t3" | awk -F : '{print $1}' 
echo -e "A:a\t1\nB:b\t2\nC:c\t3" | awk -F ":" '{print $2}' 
echo -e "A:a\t1\nB:b\t2\nC:c\t3" | awk -v FS=":" '{print $1}' 
 
echo -e "A:a\t1\nB:b\t2\nC:c\t3" | awk -v RS="\t" '{print $1}' 
echo -e "A:a\t1\nB:b\t2\nC:c\t3" | awk -v RS="\t" -v ORS="New_ORS" '{print $1}' 

AWK内置变量

Name(名称) Default value(默认值) Description(描述)
FS 空格、制表符、换行符 输入文件字段的分隔符
OFS 空格 输出文件字段的分隔符
RS 换行符 输入文件记录的分隔符
ORS 换行符 输出文件记录的分隔符
NR 当前输入记录的序号,从第一个文件第一条记录算起
FNR 当前输入记录在当前文件中的序号,从当前文件第一条记录算起
NF 当前输入记录的总的字段的数量
BINMODE
CONVFMT
FIELDWIDTHS
FPAT
IGNORECASE
LINT
OFMT
PREC
ROUNDMODE
SUBSEP
TEXTDOMAIN
ARGV 命令行参数
ARGC 命令行参数个数
awk 'BEGIN{for(i=0; i < ARGV; i++) print ARGV[i]}' 参数1 参数2  
ARGIND
ENVIRON 相关的环境变量。
awk 'BEGIN{print ENVIRON["HOME"]}' 
ERRNO
FILENAME
FUNCTAB
PROCINFO
RLENGTH
RSTART
RT
SYMTAB

AWK运算符

分类 符号 说明
数学运算符 + 、 - 、 * 、 / 、 % 、 ** 、 ^ 加、减、乘、除以、取模、指数、指数
关系运算符 == 、 != 、 < 、 ⇐ 、 > 、 >= 等于、不等于、小于、小于等于、大于、大于等于
awk 'BEGIN{print 2 == 2 }'   
awk 'BEGIN{print 2 >  3 }'   
赋值运算符 =、+=、-=、*=、/=、%=、**= 赋值、加后赋值、减后赋值、乘以后赋值、除以后赋值、取模后赋值、指数后赋值
awk 'BEGIN { a = 5; a   = 2;  print "a = "a }' 
awk 'BEGIN { a = 5; a  += 2;  print "a = "a }' 
awk 'BEGIN { a = 5; a  -= 2;  print "a = "a }' 
awk 'BEGIN { a = 5; a  *= 2;  print "a = "a }' 
awk 'BEGIN { a = 5; a  /= 2;  print "a = "a }' 
awk 'BEGIN { a = 5; a  %= 2;  print "a = "a }' 
awk 'BEGIN { a = 5; a  ^= 2;  print "a = "a }'  
++ 、 -- +1、-1,放在变量前先加或减再使用值,放在变量后则先用变量的值再加或减
awk 'BEGIN{a = 6; b = ++ a; print "a="a"\nb="b"\n" }' 
awk 'BEGIN{a = 6; b = a ++; print "a="a"\nb="b"\n" }' 
awk 'BEGIN{a = 6; b = -- a; print "a="a"\nb="b"\n" }' 
awk 'BEGIN{a = 6; b = a --; print "a="a"\nb="b"\n" }'  
逻辑运算 &&、||、! 且、或、非
三元运算符 条件表达式 ? 陈述1 : 陈述2 条件表达式结果为true时执行陈述1,不然执行陈述2
一元算符 + 、- 乘以 +1 、乘以 -1
数组元素运算符 in 存在于
awk 'BEGIN {arr["A"] = 1; arr["X"]; print "A" in arr}'
正则表达式运算符 ~、!~ 匹配、不匹配

流程控制

这与其它编程语言的类似,这里以例子的形式介绍。

for

awk 'BEGIN{for(i=1; i < 5; ++i) print i }'
awk 'BEGIN{for(i in ENVIRON) print i " is " ENVIRON[i] "\n" }' 

while

awk 'BEGIN {i = 1; while (i <= 4) { print i; ++i } }'

do-while

awk 'BEGIN {i = 1; do { print i; ++i } while (i <= 4) }'

switch

能够进行等值比较或者正则匹配。 各个分支需要用break来终止。

awk '{
  switch($1){
    case 1:
      print "星期一"
      break
    case 2:
      print "星期二"
      break
    case 3:
      print "星期三"
      break
    case 4:
      print "星期四"
      break
    case 5:
      print "星期五"
      break
    case 6:
      print "星期六"
      break
    case 7:
      print "星期日"
      break
    default:
      print "请输入数字1~7!"
      break
  }
}'

这里没有给定输入文件,接受键盘输入,运行时输入数字,就打印出星期。试试看。最后用Ctrl+c来终止程序的运行。

break

awk 'BEGIN {
  X = 0 
  for (i = 0; i < 10; ++i) {
    X += i*5 
    print "i=", i, ", X =", X
    if (X > 50) break 
  }
}'

当满足条件的时候break,不再继续执行。

continue

看看下边continue不同的位置的效果:

awk 'BEGIN {
   for (i = 1; i <= 50; ++i) {
      if (i % 5 == 0) print i
      else continue
      print "OK, i=", i
   } 
}'

这时候,当i不能被5整除的时候,就跳到下一循环了,因此,只有当i能被5整除的时候会执行后边的print “OK, i=”, i。

awk 'BEGIN {
   for (i = 1; i <= 50; ++i) {
      if (i % 5 == 0) print i
      continue
      print "OK, i=", i
   } 
}'

这时候当I能够被5整除的时候print i,然后不管i怎么样都会continue到下一个循环,后边的print “OK, i=”, i始终不会被执行。

awk 'BEGIN {
   for (i = 1; i <= 50; ++i) {
      if (i % 5 == 0) {
        print i
        continue
      }
      print "OK, i=", i
   } 
}'

这个时候,只有当i能够被5整除的时候,才会continue到下一个循环,这样当i不能被5整除的时候,会执行后边的print “OK, i=”, i。

next

next是转到下一条记录。 continue和break是用于for、while、do…while、switch等循环。

ls -l /bin/ | awk '/awk/ {print} /gawk/ {print}'   
 
ls -l /bin/ | awk '/awk/ {print; next} /gawk/ {print}'  

nextfile

nextfile 立即停止当前语句的后续操作,并直接转到下一个文件,并进入循环顶部。

exit

直接退出AWK程序。

ls -l /bin/ | awk '/awk/ {print; exit} /gawk/ {print}'
awk 'BEGIN {
   for (i = 1; i <= 50; ++i) {
      if (i % 5 == 0) {
        print i
        exit
      }
      print "OK, i=", i
   } 
}'

exit后边加上数字,指定退出状态码

echo $?
awk 'BEGIN{exit 10}'
echo $?

if...else

if 单独使用

awk '{if($1==1) print "星期一"}'

键盘输入1能够得到输出,其它情况下没有输出。

if…else, 加上else之后else后边必须要有action,不然会报错。

awk '{
  if($1==1) print "星期一"
  else print "输入数字1,打印星期一"
}'

键盘输入1能够得到输出,其它情况下输出“输入数字1,打印星期一”。

能够多个if…else if…else……

awk '{
  if($1 == 1){
    print "星期一"
  } else if($1 == 2){
    print "星期二"
  } else if($1 == 3){
    print "星期三"
  } else if($1 == 4){
    print "星期四"
  } else if($1 == 5){
    print "星期五"
  } else if($1 == 6){
    print "星期六"
  } else if($1 == 7){
    print "星期七"
  } else {
    print "请输入数字1~7!"
  }
}'

printf

printf用于格式化的打印,语法为:

printf 格式, 项目1, 项目2, …… 

其中格式限定符以%开始,以一个格式控制字母结尾,格式限定符按顺序与后边的项目相对应。%s、%f、%d、%%等等比较常用的要记住其用法。

格式限定符 说明
%s 字符串。
%f 十进制浮点数字。%n.mf, 小数点后保留m位,整个数字占n位,长度不够则前边加空格;%0n.mf,位数不够前边用0补齐。
awk 'BEGIN{printf "%10.6f", 9.99}'  
awk 'BEGIN{printf "%010.6f", 99.99}'  
%d、%i 十进制整数部分.
awk 'BEGIN{printf "%d", 9.99}'  
awk 'BEGIN{printf "%03d", 9.99}'  
%% 一个百分号: ‘%’.
%a、%A
%c Print a number as a character;
awk 'BEGIN{printf "%c", 88}'  
%e、%E Print a number in scientific (exponential) notation.
awk 'BEGIN{printf "%4.3e", 999.99}'  
awk 'BEGIN{printf "%5.4E", 9999.99}'  
%F
%g、%G Print a number in either scientific notation or in floating-point notation, whichever uses fewer characters.
%o Print an unsigned octal integer.
%u
%x、%X

printf 并不会像print那样自动加上换行符(记录分隔符)。

sprintf

sprintf和printf的格式语法一致,不同的是printf是将得到字符串打印出来,而sprintf得到的值可以赋值给一个变量。

awk 'BEGIN{A=sprintf("%010.6f", 99.99); print(A)}'

需要注意的是,这里sprintf后边的括号是不可以省略的。

管道到sh

awk里print的内容可以通过管道传给sh执行。如下程序执行后,输入shell的命令,得到命令的的结果:

awk '{print $0 | "sh"} END {close("sh")}'

如下程序执行后,输入一个文件名,将这个文件重名问全部字母都大写的文件:

awk '{printf "mv %s %s\n", $0, toupper($0) | "sh"} END {close("sh")}'

getline

getline 有多个变体,这些getline不同的使用方式效应不同,也就是改变的变量不同。归纳如下表1):

Variant Effect awk / gawk
getline Sets $0, NF, FNR, NR, and RT awk
getline var Sets var, FNR, NR, and RT awk
getline < file Sets $0, NF, and RT awk
getline var < file Sets var and RT awk
command | getline Sets $0, NF, and RT awk
command | getline var Sets var and RT awk
command |& getline Sets $0, NF, and RT gawk
command |& getline var Sets var and RT gawk

getline返回值可以用于逻辑运算:正确读到数据,则返回值为1;读取数据遇到EOF,则返回值0;读取遇到错误则为负数,-1表示文件无法打开,-2表示IO操作需要重试;遇到错误时用变量ERRNO描述错误。

getline后边不加参数

这时候getline读取下一个记录并将它拆分成字段,这个getline命令设置了NF、NR、FNR、RT以及$0的值。

echo -e "A\t10\nB\t20\nC\t30\nD\t40" | awk '{print $0; getline}'
 
echo -e "A\t10\nB\t20\nC\t30\nD\t40" | awk '{print $0; if((getline) > 0) print "NR="NR, $0}'

可以看到getline设置了NR之后,再次print $0就到下下一个记录了。

getline到变量

getline到变量X:

echo -e "A\t10\nB\t20\nC\t30\nD\t40" | awk '{print $0; getline X; print X}'
 
echo -e "A\t10\nB\t20\nC\t30" | awk '{print $0; getline X; print X}'
 
echo -e "A\t10\nB\t20\nC\t30" | awk '{print $0; if((getline X) > 0) print X}'

可以看到,在最后一个记录的时候,getline不再能够改变X的值。当把getline放到if后边作为条件的时候,依然会执行getline。

getline自一个文件

先建两个测试文件:

echo -e "A\t10\nB\t20\nC\t30\nD\t40" > test_g.txt
echo -e "a\t01\nb\t02\nc\t03\nd\t04" > test_i.txt

可以看到,每次运行都在上次运行的下一条记录:

awk  '{
  getline < "test_g.txt"
  print $0, "NR="NR, "FNR="FNR, "NF="NF
}' test_i.txt

可以看到,getline到一个变量的时候,不改变$0

awk '{
  getline X < "test_g.txt"
  print X, "NR="NR, "FNR="FNR, "NF="NF
  print $0, "NR="NR, "FNR="FNR, "NF="NF
}' test_i.txt
awk '{
  getline X < "test_g.txt"
  print X, "NR="NR, "FNR="FNR, "NF="NF
  print $0, "NR="NR, "FNR="FNR, "NF="NF
}' test_i.txt test_i.txt

结合printf可以将两个文件逐行并列地放到一行:

awk '{
   printf "%s\t", $0
   getline < "test_g.txt"
   print $0
}'  test_i.txt

在getline之前,$0是test_i.txt文件里的记录,printf打印出来后边加个制表符\t,getline之后,$0改变了,print的时候自动加上了换行符。

可以在BEGIN里连续多次getline完成对一个文件的处理:

awk 'BEGIN{
  while(1){
    if((getline < "test_g.txt") > 0){
      print $0, "NR="NR, "FNR="FNR, "NF="NF
    } else {
      break
    }
  }
}'

可以将一个文件的信息存到一个数组里,然后再使用:

awk 'BEGIN{
  while(1){
    if((getline < "test_g.txt") > 0){
      A[$1]=$2
    } else {
      break
    }
  }
} {
  print $0, A[toupper($1)]
}' test_i.txt

从管道getline

系统命令需要用引号引起来,然后是需要close这个命令。

awk 'BEGIN{
  "date" | getline
  close("date")
  print $0
}'

AWK内置函数

数学操作

int(n)函数获取数字n的整数部分

awk 'BEGIN{print int(3.5678)}'       
awk 'BEGIN{print int(-3.5678)}'

log(n) 求数字n的自然对数

awk 'BEGIN{print log(100)}'          

sqrt(n) 求数字n的正的平方根

awk 'BEGIN{print sqrt(100)}' 

sin(n) 求n的郑玄,n为弧度数。同样有cos(n)求余弦。

awk 'BEGIN{print sin(3.14159265)}'   # 

rand() 产生0~1之间的随机数字。srand(n)初始化随机数字产生的seed,使用了srand而不设置具体数值, 每次运行得到的结果不同:可以看到每次运行得到的数字是一样的。不加上rand()则相当于设置了srand(1):

awk 'BEGIN{print rand()}'
awk 'BEGIN{print rand()}'
awk 'BEGIN{srand(1) print rand()}'
awk 'BEGIN{srand(1) print rand()}'
awk 'BEGIN{srand(2) print rand()}'
 
#如下可以产生10个随机数字:
awk 'BEGIN{for(i=1; i<= 10; i++) print rand();}'
awk 'BEGIN{for(i=1; i<= 10; i++) print rand();}'
 
# 使用了srand而不给参数,使用当前时间,每次运行结果都不一样
awk 'BEGIN{srand( ); print rand()}'
awk 'BEGIN{srand( ); print rand()}'
 
# 设置好srand,以便产生的随机数是可重复的:
awk 'BEGIN{srand(1); for(i=1; i<= 10; i++) print rand();}'
awk 'BEGIN{srand(1); for(i=1; i<= 10; i++) print rand();}'
awk 'BEGIN{srand(5); for(i=1; i<= 10; i++) print rand();}'
awk 'BEGIN{srand(5); for(i=1; i<= 10; i++) print rand();}'

字符操作

match(s,r)测试字符串s是否包含匹配正则表达式r的字符串,返回第一次匹配的开始位置,如果不存在则返回值为0。

awk 'BEGIN{print match("WangHong", "Ho")}'   

index(str1, str2) 在str1中str2第一次出现的位置,第一个字符开始的位置,如果不存在,则为0。index中str2不能是正则表达式,这是与match函数不同的。

awk 'BEGIN{print index("WangHong", "Ho")}'   

length(str) 计算字符串str的长度

awk 'BEGIN{print length("WangHong")}'        

substr(s, p, n) 从p位开始对字符串s取n个字符串,如果n未指明,则取出后边所有的字符

awk 'BEGIN{print substr("WangHong", 5, 10)}' 

tolower(str)将字符串str中的字母转为小写,与之对应的,toupper(str)为转为大写

awk 'BEGIN{print tolower("WangHong")}'    

split(string, array, fieldsep) 将字符串string以fieldsep为分隔符拆分为字段,存为数组array,片段依次存为array[1]、array[2]、……

awk 'BEGIN{split("WangHong", X, "n"); for(i in X) print X[i]}' 

sub(r, t, s)将字符串s中第一次出现的匹配正则表达式r的部分替换为t,如果不指定s,则对$0进行替换。

awk 'BEGIN{XM="WangHong"; sub("ng", "XX", XM); print XM}' 

gsub(r, t, s)将字符串s中所有匹配正则表达式r的部分都替换为t,ruguo不指定s,则对$0进行替换。

awk 'BEGIN{XM="WangHong"; gsub("ng", "XX", XM); print XM}' 

sprintf(format, expression1, …)返回格式化的字符串,与printf不同的是sprintf并不打印出来。

awk 'BEGIN{Pi = sprintf("pi ≈ %.2f", 22/7); print Pi}'

标题

 
 
 
 

新增

 

运用示例

从fasta文件中提取特定ID的序列

比如现在有一个fasta文件,另有一个文件记录我们要提取的序列的ID,这时候使用AWK就会非常简单,示例文件test.fa与ID.txt

test.fa
>ID01 some description 01
GCGCGCGGTCACGTTTTCCACTATGTGACAGCGGAGGGCGACGCGGCGGCAGCGGCGCTACTGGGACTAG
>ID02 some description 02
CGGCTCCGGGCGGCTGCGGCGCAGGCCGAGCGCACCAAGTGACGGGCCGAGCAAGGGACAGACGCGCGGG
TTGACGCGGCGAAGCGCTTATTCCAGAGCCCGACATGAATGGATACGTGGACTTCTCCCCAAGTCCCACC
AGTCCCACCAAGGAGCCAGGGGCACCTCAGCCCACCCAGGCTGTGCTCCAGGAAGACGTGGACATGAGCA
GTGGCTCCAGCGGAAACGAGAACTGCTCCACGGGACGGGACTCTCAGGGCAGTGACTGCGACGACAATGG
>ID03 some description 03
GAAGGAGCTGCGGATGCTCGTGGAATCTTCCAACACTCACCCCAGCCCTGATGATGCCTTCAGACTCATG
ATGACAGAGGCAGAGCACAACCCCTCCACGAGCGGCTGCAGTAGTGAGCAGTCTGCCAAAGCTGACGCAC
>ID04 some description 04
ACAAAGAACTGATAAGGACCCTGAAGGAGCTGAAGGTCCACCTCCCTGCAGACAAGAAGGCCAAGGGGAA
>ID05 some description 05
GGCCAGCACGCTGGCAACCTTGAAGTATGCCCTGCGGAGCGTGAAGCAGGTGAAGGCTAATGAGGAGTAC
>ID06 some description 06
TACCAGCTGCTAATGTCCAGTGAGAGCCAGCCCTGCAGTGTGGATGTGCCTTCCTACAGCATGGAGCAGG
TTGAGGGCATTACCTCCGAGTATATCGTGAAGAACGCGGATATGTTTGCTGTGGCTGTGTCCCTGGTTTC
>ID07 some description 07
TGGGAAGATCCTGTACATCTCTAACCAAGTGGCCTCCATCTTTCACTGTAAGAAGGACGCCTTCAGTGAT
>ID08 some description 08
GCCAAGTTTGTGGAGTTCCTGGCTCCTCATGACGTCAGTGTGTTCCACAGCTACACCACCCCTTACAAGC
>ID09 some description 09
TTCCGCCCTGGAGTGTGTGCAGCGGCTTAGATTCTTTCACTCAGGAGTGCATGGAGGAGAAATCTTTTTT
ID.txt
ID02
ID04
ID09

自己创建上边两个文件用于测试。 获取ID.txt里的ID的序列的代码如下,试试看是不是得到了自己想要的结果:

awk -v RS=">" 'NR > 1 && NR == FNR {IDs[$1];next} $1 in IDs {printf ">"$0}'  \
  <(awk '{print ">"$0}' ID.txt)  test.fa

这里以>作为记录的分隔符,这样每条序列就是一个记录,默认是以空格分隔字段,ID正好是第一个字段。而输出时默认的记录分隔是换行符,因此也要指明ORS。
因为ID.txt里的ID之间的分隔是换行符,为了能够使用前边设置的-v RS=“>”,这里给它加上, 也就是<(awk '{print “>“$0}' ID.txt)所干的活。
这里有两个输入文件,NR以所有文件作为一个整体进行连续计数,而FNR对每个文件从1开始,所以对于第一个文件,FNR是等于NR的,从第二个文件开始,FNR就和NR不相等了。
所以,这里运行过程就是对第一个文件,把每个记录的第一个字段作为数组(阵列)IDs的索引(Index)而记录下来,然后next到下一条记录,等到了第二个文件的时候,FNR就不等于NR了,于是就执行后边的,判断第一个字段是否能够在字典中找到,能找到就把整个记录print出来。
这里使用printf,因为print默认在每个记录后边加上一个RS,而fasta文件的>是加在前边的。

求fasta文件中各条序列的长度和GC含量

比如还是用上边例子中的test.fa文件:

awk -v RS=">" -v FS="\n" -v OFS="\t" '
  BEGIN{print "transcript_id\ttranscript_length\tgc_count\tgc_content"} 
  NR > 1 {
  Tid = $1; 
  $1  = ""; 
  gsub("\t", "", $0);
  nnN = length($0);
  gsub("G|C", "", $0);
  gcN = nnN - length($0);
  gcR = sprintf("%.4f", gcN/nnN);
  print Tid,nnN,gcN,gcR}' test.fa  \
> test.transcript_info.tsv

其逻辑是,以>来分隔各条序列,那么第一条记录(Record)是空的,序列的信息从第二条记录开始,字段(Field)则以换行符\n分隔,第一个字段($1)是序列的ID,后边的是序列。记录下之后把$1变成空之后,$0就只包含序列和字段分隔符了(这时候的分隔符是输出的分隔符OFS\t);然后去掉\t之后就是整条序列了,可以算得整条序列的长度;然后将G和C去掉,然后其长度就是没有GC的长度,减一下就可以得到GC的数量;最后可以算得GC含量。
得到的结果如下:

test.transcript_info.tsv
transcript_id   transcript_length       gc_count        gc_content
ID01 some description 01        70      48      0.6857
ID02 some description 02        280     184     0.6571
ID03 some description 03        140     81      0.5786
ID04 some description 04        70      37      0.5286
ID05 some description 05        70      41      0.5857
ID06 some description 06        140     75      0.5357
ID07 some description 07        70      33      0.4714
ID08 some description 08        70      38      0.5429
ID09 some description 09        70      34      0.4857

如果transcript_id里边含有制表符,那么可以把制表符替换成别的,不然将会影响列的区分。当然也可以使用别的符号来分隔表格。

当然也可以不改变$0来完成任务,但是运行起来似乎会慢一些:

awk -v RS=">" -v FS="\n" -v OFS="\t" '
  NR > 1 {
  Seq = ""
  for(i=2; i<=NF; i++) Seq = Seq$i;
  nnN = length(Seq);
  gsub("G|C", "", Seq);
  gcN = nnN - length(Seq);
  gcR = sprintf("%.4f", gcN/nnN);
  print $1,nnN,gcN,gcR}' test.fa  \
> test.transcript_info.tsv   

将fasta文件拆分成特定数量条数的文件

比如,我有一个81415条序列的fasta文件(比较叫ORFs.fa),我想要拆分开用于功能注释,那么可以这样子拆分成每个含1000条序列的文件,文件名以:

awk -v RS=">" -v ORS="" 'NR != 1 {
  FileName=sprintf("ORFs_%02d.fa", int((NR-1)/1000));
  print ">"$0 > FileName
}' ORFs.fa

本网页已被访问次数(Times visited):12369; 最后编辑时间(Last modified): 2024/11/12 16:21