【Linux进阶】使用grep、find、sed以及awk进行文本操作

一、元字符

详情请见匹配规则

二、grep命令

下面通过实战演示的方式介绍grep命令的常见用法,为此首先我们需要一个示例文本文件,这里使用CentOS操作系统/etc目录下的passwd文件:

[root@iZbp1gjysfmcbcojeshiw7Z python2.7]# lsb_release -a
LSB Version:    :core-4.1-amd64:core-4.1-noarch
Distributor ID: CentOS
Description:    CentOS Linux release 8.2.2004 (Core) 
Release:        8.2.2004
Codename:       Core
[root@iZbp1gwkhxi6nj3ztmah8uZ ~]# cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
systemd-coredump:x:999:997:systemd Core Dumper:/:/sbin/nologin
systemd-resolve:x:193:193:systemd Resolver:/:/sbin/nologin
tss:x:59:59:Account used by the trousers package to sandbox the tcsd daemon:/dev/null:/sbin/nologin
polkitd:x:998:996:User for polkitd:/:/sbin/nologin
libstoragemgmt:x:997:995:daemon account for libstoragemgmt:/var/run/lsm:/sbin/nologin
unbound:x:996:993:Unbound DNS resolver:/etc/unbound:/sbin/nologin
setroubleshoot:x:995:991::/var/lib/setroubleshoot:/sbin/nologin
cockpit-ws:x:994:990:User for cockpit web service:/nonexisting:/sbin/nologin
cockpit-wsinstance:x:993:989:User for cockpit-ws instances:/nonexisting:/sbin/nologin
sssd:x:992:988:User for sssd:/:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
chrony:x:991:987::/var/lib/chrony:/sbin/nologin
rngd:x:990:986:Random Number Generator Daemon:/var/lib/rngd:/sbin/nologin
tcpdump:x:72:72::/:/sbin/nologin
nscd:x:28:28:NSCD Daemon:/:/sbin/nologin

1. 过滤出包含某字符串的行

[root@iZbp1gwkhxi6nj3ztmah8uZ ~]# grep "Kernel" /etc/passwd
nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin

# 忽略大小写
[root@iZbp1gwkhxi6nj3ztmah8uZ ~]# grep -i "kernel" /etc/passwd
nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin

# 同时输出行号
[root@iZbp1gwkhxi6nj3ztmah8uZ ~]# grep -i -n "kernel" /etc/passwd
13:nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin

2. 过滤出以某字符串开头(结尾)的行

# 过滤出以sshd开头的行
[root@iZbp1gwkhxi6nj3ztmah8uZ ~]# grep "^sshd" /etc/passwd
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin

# 过滤出以shutdown结尾的行
[root@iZbp1gwkhxi6nj3ztmah8uZ ~]# grep "shutdown$" /etc/passwd
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown

3. 过滤出包含某字符串及其相邻的行

# 将包含Kernel的行以及其下边的一行过滤出来
[root@iZbp1gwkhxi6nj3ztmah8uZ ~]# grep -i -A1  "kernel" /etc/passwd
nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin

# 将包含Kernel的行以及其上边的一行过滤出来
[root@iZbp1gwkhxi6nj3ztmah8uZ ~]# grep -i -B1  "kernel" /etc/passwd
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin

# 将包含Kernel的行以及其上边和下边的一行过滤出来
[root@iZbp1gwkhxi6nj3ztmah8uZ ~]# grep -i -C1  "kernel" /etc/passwd
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin

4. 过滤出不包含某关键字的行

# 过滤出不包含nologin的行,并输出其行号
[root@iZbp1gwkhxi6nj3ztmah8uZ ~]# grep -i -v -n "nologin" /etc/passwd
1:root:x:0:0:root:/root:/bin/bash
6:sync:x:5:0:sync:/sbin:/bin/sync
7:shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
8:halt:x:7:0:halt:/sbin:/sbin/halt

5. 过滤出包含多个字符串中任意一个的行

# 过滤出包含shutdown或kernel的行
[root@iZbp1gwkhxi6nj3ztmah8uZ ~]# grep -i -e "shutdown" -e  "kernel" /etc/passwd
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin

# 上述命令等价于下列命令
[root@iZbp1gwkhxi6nj3ztmah8uZ ~]# grep -i -E "shutdown|kernel" /etc/passwd
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin

6. 查看目录中包含某字符串的所有文件

# 递归查询/etc/目录下包含"nobody"在内的所有文件
[root@iZbp1gwkhxi6nj3ztmah8uZ ~]# grep -r -n "nobody" /etc/
/etc/ssh/sshd_config:60:#AuthorizedKeysCommandUser nobody
/etc/aliases:29:nobody:         root
/etc/aliases:65:nfsnobody:      root
/etc/group-:24:nobody:x:65534:
/etc/gshadow-:24:nobody:::
/etc/passwd-:13:nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin
/etc/shadow-:13:nobody:*:18358:0:99999:7:::
/etc/group:24:nobody:x:65534:
/etc/gshadow:24:nobody:::
/etc/passwd:13:nobody:x:65534:65534:Kernel Overflow User:/:/sbin/nologin
/etc/shadow:13:nobody:*:18358:0:99999:7:::
/etc/idmapd.conf:43:#Nobody-User = nobody
/etc/idmapd.conf:44:#Nobody-Group = nobody
/etc/pinforc:93:SAFE-USER=nobody
/etc/pinforc:94:SAFE-GROUP=nobody

三、find命令

参考Linux 文件搜索神器 find 实战详解,建议收藏!

1. 按文件名查找

  • 查找当前目录下所有 c 语言文件:
[root@iZbp1gjysfmcbcojeshiw7Z python2.7]# pwd
/usr/local/aegis/PythonLoader/lib/python2.7
[root@iZbp1gjysfmcbcojeshiw7Z python2.7]# find ./ -name "*.c"
./config/config.c
./distutils/tests/xxmodule.c
[root@iZbp1gjysfmcbcojeshiw7Z python2.7]# find ./ -name *.c
./config/config.c
./distutils/tests/xxmodule.c
  • 在当前目录下,查找大写字母开头的 txt 文件:
[root@iZbp1gjysfmcbcojeshiw7Z lib2to3]# pwd
/usr/lib64/python3.6/lib2to3
[root@iZbp1gjysfmcbcojeshiw7Z lib2to3]# find ./ -name "[A-Z]*.txt"
./Grammar.txt
./PatternGrammar.txt
[root@iZbp1gjysfmcbcojeshiw7Z lib2to3]# find ./ -name "[A-Z]*.txt" -print
./Grammar.txt
./PatternGrammar.txt

其中 -print 表示行为(即 action ),默认指定; find 命令另一个常用的行为是 -prune ,表示不搜索某一个目录。

  • 在当前目录下,查找不是以 fix 开头的 .py 文件:
[root@iZbp14vmgrtj1265z7za9nZ fixers]# pwd
/usr/local/aegis/PythonLoader/lib/python2.7/lib2to3/tests/data/fixers
[root@iZbp14vmgrtj1265z7za9nZ fixers]# 
[root@iZbp14vmgrtj1265z7za9nZ fixers]# find ./ -name "*.py" -print
./parrot_example.py
./myfixes/__init__.py
./myfixes/fix_first.py
./myfixes/fix_preorder.py
./myfixes/fix_explicit.py
./myfixes/fix_last.py
./myfixes/fix_parrot.py
./no_fixer_cls.py
./bad_order.py
[root@iZbp14vmgrtj1265z7za9nZ fixers]# find ./ -name "fix*" -prune -o -name "*.py" -print
./parrot_example.py
./myfixes/__init__.py
./no_fixer_cls.py
./bad_order.py
  • 在当前目录下,查找不在 myfixes 目录下的 .py 文件:
[root@iZbp14vmgrtj1265z7za9nZ fixers]# find ./ -path "./myfixes" -prune -o -name "*.py" -print
./parrot_example.py
./no_fixer_cls.py
./bad_order.py

需要注意的是,当希望按照文件名称搜索时忽略大小写,则应该使用选项 -iname 而不是 -name

2. 按文件类型查找

  • 在当前目录下,查找软连接文件,且指定最大递归深度为1:
[root@iZbp14vmgrtj1265z7za9nZ /]# pwd
/
[root@iZbp14vmgrtj1265z7za9nZ /]# find ./ -maxdepth 1 -type l -print
./bin
./sbin
./lib
./lib64
  • 在当前目录下,查找 log 结尾的普通文件,f 表示普通文件类型:
[root@iZbp14vmgrtj1265z7za9nZ /]# find ./ -type f -a -name "*.log"

3. 按文件大小查找

  • 查找大小超过 64k 的文件:
[root@iZbp14vmgrtj1265z7za9nZ ssh]# pwd
/etc/ssh
[root@iZbp14vmgrtj1265z7za9nZ ssh]# find . -size +64k -print
./moduli

4. 按文件时间查找

  • 查找当前目录下:
    • 修改时间在48小时以上的普通文件;
    • 修改时间在72小时以上的普通文件;
    • 修改时间在24小时以内的普通文件;
    • 修改时间在24小时以上,48小时以内的普通文件。
[root@iZbp14vmgrtj1265z7za9nZ ~]# find . -mtime +1 -type f -print
[root@iZbp14vmgrtj1265z7za9nZ ~]# find . -mtime +2 -type f -print
[root@iZbp14vmgrtj1265z7za9nZ ~]# find . -mtime -1 -type f -print
[root@iZbp14vmgrtj1265z7za9nZ ~]# find . -mtime -2 -type f -print
[root@iZbp14vmgrtj1265z7za9nZ ~]# find . -mtime 1 -type f -print

实际上,对于选项 -atime-ctime 也有类似的语法。

  • 查找比 ssh_host_rsa_key 新或旧的文件
[root@iZbp14vmgrtj1265z7za9nZ ssh]# pwd
/etc/ssh
[root@iZbp14vmgrtj1265z7za9nZ ssh]# find ./ -newer "ssh_host_rsa_key" -type f -print
./sshd_config
./ssh_host_dsa_key
./ssh_host_dsa_key.pub
./ssh_host_ecdsa_key
./ssh_host_ecdsa_key.pub
./ssh_host_ed25519_key
./ssh_host_ed25519_key.pub
[root@iZbp14vmgrtj1265z7za9nZ ssh]# find ./ ! -newer "ssh_host_rsa_key" -type f -print
./moduli
./ssh_config
./ssh_config.d/05-redhat.conf
./ssh_host_rsa_key
./ssh_host_rsa_key.pub

5. 按文件权限查找

[root@iZbp14vmgrtj1265z7za9nZ ssh]# pwd
/etc/ssh
[root@iZbp14vmgrtj1265z7za9nZ ssh]# ll
total 608
-rw-r--r--. 1 root root 577388 Feb  5  2020 moduli
-rw-r--r--. 1 root root   1716 Feb  5  2020 ssh_config
drwxr-xr-x. 2 root root     28 Nov 20  2020 ssh_config.d
-rw-------  1 root root   4296 Jun  1 14:27 sshd_config
-rw-------  1 root root   1401 Jun  1 14:26 ssh_host_dsa_key
-rw-r--r--  1 root root    618 Jun  1 14:26 ssh_host_dsa_key.pub
-rw-------  1 root root    525 Jun  1 14:26 ssh_host_ecdsa_key
-rw-r--r--  1 root root    190 Jun  1 14:26 ssh_host_ecdsa_key.pub
-rw-------  1 root root    419 Jun  1 14:26 ssh_host_ed25519_key
-rw-r--r--  1 root root    110 Jun  1 14:26 ssh_host_ed25519_key.pub
-rw-------  1 root root   2622 Jun  1 14:26 ssh_host_rsa_key
-rw-r--r--  1 root root    582 Jun  1 14:26 ssh_host_rsa_key.pub
[root@iZbp14vmgrtj1265z7za9nZ ssh]# find . -type d -a -perm 755
.
./ssh_config.d
[root@iZbp14vmgrtj1265z7za9nZ ssh]# find . -type f -perm 644
./moduli
./ssh_config
./ssh_config.d/05-redhat.conf
./ssh_host_rsa_key.pub
./ssh_host_dsa_key.pub
./ssh_host_ecdsa_key.pub
./ssh_host_ed25519_key.pub
[root@iZbp14vmgrtj1265z7za9nZ ssh]# find . -type f -perm 600
./sshd_config
./ssh_host_rsa_key
./ssh_host_dsa_key
./ssh_host_ecdsa_key
./ssh_host_ed25519_key
  • 查找当前目录下所有用户都有执行权限的文件:
[root@iZbp14vmgrtj1265z7za9nZ ssh]# find ./ -perm -111
./
./ssh_config.d
  • 查找当前目录下至少一个用户有写权限的文件:
[root@iZbp14vmgrtj1265z7za9nZ ssh]# ll | sed '1d' | nl
     1  -rw-r--r--. 1 root root 577388 Feb  5  2020 moduli
     2  -rw-r--r--. 1 root root   1716 Feb  5  2020 ssh_config
     3  drwxr-xr-x. 2 root root     28 Nov 20  2020 ssh_config.d
     4  -rw-------  1 root root   4296 Jun  1 14:27 sshd_config
     5  -rw-------  1 root root   1401 Jun  1 14:26 ssh_host_dsa_key
     6  -rw-r--r--  1 root root    618 Jun  1 14:26 ssh_host_dsa_key.pub
     7  -rw-------  1 root root    525 Jun  1 14:26 ssh_host_ecdsa_key
     8  -rw-r--r--  1 root root    190 Jun  1 14:26 ssh_host_ecdsa_key.pub
     9  -rw-------  1 root root    419 Jun  1 14:26 ssh_host_ed25519_key
    10  -rw-r--r--  1 root root    110 Jun  1 14:26 ssh_host_ed25519_key.pub
    11  -rw-------  1 root root   2622 Jun  1 14:26 ssh_host_rsa_key
    12  -rw-r--r--  1 root root    582 Jun  1 14:26 ssh_host_rsa_key.pub
[root@iZbp14vmgrtj1265z7za9nZ ssh]# find ./ -maxdepth 1 -perm /222 | nl
     1  ./
     2  ./moduli
     3  ./ssh_config
     4  ./ssh_config.d
     5  ./sshd_config
     6  ./ssh_host_rsa_key
     7  ./ssh_host_rsa_key.pub
     8  ./ssh_host_dsa_key
     9  ./ssh_host_dsa_key.pub
    10  ./ssh_host_ecdsa_key
    11  ./ssh_host_ecdsa_key.pub
    12  ./ssh_host_ed25519_key
    13  ./ssh_host_ed25519_key.pub

6. 按组合条件查找

  • 查找当前目录下,所属用户为 root 的目录:
[root@iZbp14vmgrtj1265z7za9nZ ssh]# pwd
/etc/ssh
[root@iZbp14vmgrtj1265z7za9nZ ssh]# find . -type d -a -user root -print
.
./ssh_config.d
  • 查找当前目录下的非普通文件:
[root@iZbp14vmgrtj1265z7za9nZ ssh]# find ./ -not -type f
./
./ssh_config.d
[root@iZbp14vmgrtj1265z7za9nZ ssh]# 
[root@iZbp14vmgrtj1265z7za9nZ ssh]# find ./ ! -type f
./
./ssh_config.d
  • 查找当前目录下的非空文件:
[root@iZbp14vmgrtj1265z7za9nZ ssh]# find . ! -empty

7. 查找出文件后做相应处理

通过 find 命令查找出某个文件之后,我们可以继续使用 -exec-ok ,对其进行进一步的处理,如:

[root@iZbp14vmgrtj1265z7za9nZ ssh]# find ./ -name "*.pub" -exec ls -alh {} \;
-rw-r--r-- 1 root root 582 Jun  1 14:26 ./ssh_host_rsa_key.pub
-rw-r--r-- 1 root root 618 Jun  1 14:26 ./ssh_host_dsa_key.pub
-rw-r--r-- 1 root root 190 Jun  1 14:26 ./ssh_host_ecdsa_key.pub
-rw-r--r-- 1 root root 110 Jun  1 14:26 ./ssh_host_ed25519_key.pub
[root@iZbp14vmgrtj1265z7za9nZ ssh]# find ./ -name "*.pub" -ok ls -alh {} \;
< ls ... ./ssh_host_rsa_key.pub > ? n
< ls ... ./ssh_host_dsa_key.pub > ? n
< ls ... ./ssh_host_ecdsa_key.pub > ? y
-rw-r--r-- 1 root root 190 Jun  1 14:26 ./ssh_host_ecdsa_key.pub
< ls ... ./ssh_host_ed25519_key.pub > ? y
-rw-r--r-- 1 root root 110 Jun  1 14:26 ./ssh_host_ed25519_key.pub

由上述可知: -ok-exec 功能一样,只是前者操作时会提示用户确认,仅此而已。当然,在生产环境上,还是推荐使用 -ok

在这里说明一下{}\;

  • {}其实它就是一个占位符,在 find 命令的执行过程中会不断地替换成当前找到的文件,相当于ls -l 找到的文件
  • \;-exec 的命令结束标记,因为规定 -exec 后面的命令必须以 ; 结束,但 ; 在 shell 中有特殊含义,必须要转义,所以写成 \;

四、sed命令

参考干货!上古神器 sed 教程详解,小白也能看的懂

1. sed简介

  • sed 全名叫 stream editor,即流编辑器,其使用方式与交互式文本编辑器截然不同的。 在使用交互式文本编辑器如 vim 时,用户使用键盘通过交互的方式插入、删除、替换文本;
  • sed 这样的流编辑器使用提前编写好的一系列命令来编辑文本,这些命令在编辑器处理文本之前就需要写好
  • 因为这样的使用方式, sed 尤其适用于某些情况下的文本编辑,如:希望通过自动化的方式批量修改大量的待编辑文本。

2. 工作流程

针对 sed 这样的流编辑器,其工作流程大致如下:

  • 从输入中一次读入一行数据;
  • 将该行数据依次和预先写好的命令进行匹配;
  • 当命令匹配成功时,对流中的数据进行相应修改;
  • 将修改后的数据输出至STDOUT

3. 基本语法

4. 案例实战

首先,为了便于后面演示,通过下列命令创建一个测试文本 sed_demo.txt

[root@iZbp15spmmi74px0lk8l6nZ ~]# head -n5 /etc/passwd > sed_demo.txt 
[root@iZbp15spmmi74px0lk8l6nZ ~]# cat -n sed_demo.txt 
     1  root:x:0:0:root:/root:/bin/bash
     2  bin:x:1:1:bin:/bin:/sbin/nologin
     3  daemon:x:2:2:daemon:/sbin:/sbin/nologin
     4  adm:x:3:4:adm:/var/adm:/sbin/nologin
     5  lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

定址

默认情况下,sed 会对文本的每一行都执行读取、匹配、操作、输出这些步骤,但很多时候我们只想对部分行执行这些步骤,而定位期望处理的目标行就叫做定址,根据方式不同,又可分为数字定址正则定址

数字定址

数字定址顾名思义就是通过数字的方式确定文本要处理的行:

  • 仅将第1行中的所有 root 替换为 ROOT
[root@iZbp15spmmi74px0lk8l6nZ ~]# sed '1s/root/ROOT/g' sed_demo.txt | nl
     1  ROOT:x:0:0:ROOT:/ROOT:/bin/bash
     2  bin:x:1:1:bin:/bin:/sbin/nologin
     3  daemon:x:2:2:daemon:/sbin:/sbin/nologin
     4  adm:x:3:4:adm:/var/adm:/sbin/nologin
     5  lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
  • 仅将第2-4行中的所有 sbin 替换为 SBIN
[root@iZbp15spmmi74px0lk8l6nZ ~]# sed '2,4s/sbin/SBIN/g' sed_demo.txt | nl
     1  root:x:0:0:root:/root:/bin/bash
     2  bin:x:1:1:bin:/bin:/SBIN/nologin
     3  daemon:x:2:2:daemon:/SBIN:/SBIN/nologin
     4  adm:x:3:4:adm:/var/adm:/SBIN/nologin
     5  lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
  • 仅将从第2行开始,往下数3行,即2-5行中的所有 sbin 替换为 SBIN
[root@iZbp15spmmi74px0lk8l6nZ ~]# sed '2,+3s/sbin/SBIN/g' sed_demo.txt | nl
     1  root:x:0:0:root:/root:/bin/bash
     2  bin:x:1:1:bin:/bin:/SBIN/nologin
     3  daemon:x:2:2:daemon:/SBIN:/SBIN/nologin
     4  adm:x:3:4:adm:/var/adm:/SBIN/nologin
     5  lp:x:4:7:lp:/var/spool/lpd:/SBIN/nologin
  • 将除第3行以外所有行中的 sbin 替换为 SBIN
[root@iZbp15spmmi74px0lk8l6nZ ~]# sed '3!s/sbin/SBIN/g' sed_demo.txt | nl
     1  root:x:0:0:root:/root:/bin/bash
     2  bin:x:1:1:bin:/bin:/SBIN/nologin
     3  daemon:x:2:2:daemon:/sbin:/sbin/nologin
     4  adm:x:3:4:adm:/var/adm:/SBIN/nologin
     5  lp:x:4:7:lp:/var/spool/lpd:/SBIN/nologin

正则定址

  • 仅将最后一行中的所有 sbin 替换为 SBIN
[root@iZbp15spmmi74px0lk8l6nZ ~]# sed '$s/sbin/SBIN/g' sed_demo.txt | nl
     1  root:x:0:0:root:/root:/bin/bash
     2  bin:x:1:1:bin:/bin:/sbin/nologin
     3  daemon:x:2:2:daemon:/sbin:/sbin/nologin
     4  adm:x:3:4:adm:/var/adm:/sbin/nologin
     5  lp:x:4:7:lp:/var/spool/lpd:/SBIN/nologin
  • 将匹配到以 daemon 开头的行到以 adm 开头的行及其之间的所有行进行删除:
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed '/^daemon/,/^adm/d' sed_demo.txt | nl
     1  root:x:0:0:root:/root:/bin/bash
     2  bin:x:1:1:bin:/bin:/sbin/nologin
     3  lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
  • 删除文档中的所有空行:
[root@iZbp142l91zbbe0hqz89bgZ ~]# cat -n sed_demo.txt 
     1  root:x:0:0:root:/root:/bin/bash
     2  bin:x:1:1:bin:/bin:/sbin/nologin
     3
     4
     5  daemon:x:2:2:daemon:/sbin:/sbin/nologin
     6
     7  adm:x:3:4:adm:/var/adm:/sbin/nologin
     8  lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed '/^$/d' sed_demo.txt | nl
     1  root:x:0:0:root:/root:/bin/bash
     2  bin:x:1:1:bin:/bin:/sbin/nologin
     3  daemon:x:2:2:daemon:/sbin:/sbin/nologin
     4  adm:x:3:4:adm:/var/adm:/sbin/nologin
     5  lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

数字定址和正则定址混用

  • 匹配从第1行到以 adm 开头的行,并将匹配的行进行删除:
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed '1,/^adm/d' sed_demo.txt | nl
     1  lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

基本子命令

追加行子命令 a

  • 在所有行下方追加 /etc/passwd
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed 'a /etc/passwd' sed_demo.txt 
root:x:0:0:root:/root:/bin/bash
/etc/passwd
bin:x:1:1:bin:/bin:/sbin/nologin
/etc/passwd
daemon:x:2:2:daemon:/sbin:/sbin/nologin
/etc/passwd
adm:x:3:4:adm:/var/adm:/sbin/nologin
/etc/passwd
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
/etc/passwd
  • 仅在第1,2行之后追加/etc/passwd
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed '1,2a /etc/passwd' sed_demo.txt 
root:x:0:0:root:/root:/bin/bash
/etc/passwd
bin:x:1:1:bin:/bin:/sbin/nologin
/etc/passwd
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
  • 仅将第1行及其向下两行后追加 /etc/passwd
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed '1,+2a /etc/passwd' sed_demo.txt 
root:x:0:0:root:/root:/bin/bash
/etc/passwd
bin:x:1:1:bin:/bin:/sbin/nologin
/etc/passwd
daemon:x:2:2:daemon:/sbin:/sbin/nologin
/etc/passwd
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
  • 仅在最后一行后追加 /etc/passwd
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed '$a /etc/passwd' sed_demo.txt 
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
/etc/passwd

插入行子命令 i

子命令 ia 用法基本一样,只不过 i 是在指定行上方插入指定的内容行:

[root@iZbp142l91zbbe0hqz89bgZ ~]# sed '$i /etc/passwd' sed_demo.txt 
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
/etc/passwd
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

替换子命令 s

上面的案例中,通过定址指定的待操作行之后的 s 即表示替换,这也是 sed 最常用的一种功能:

其基本语法为:

sed [address] s /pat/rep/flags

  • 仅将每行匹配到的第一个 bin 替换为 BIN
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed 's/bin/BIN/' sed_demo.txt | nl
     1  root:x:0:0:root:/root:/BIN/bash
     2  BIN:x:1:1:bin:/bin:/sbin/nologin
     3  daemon:x:2:2:daemon:/sBIN:/sbin/nologin
     4  adm:x:3:4:adm:/var/adm:/sBIN/nologin
     5  lp:x:4:7:lp:/var/spool/lpd:/sBIN/nologin
  • 将每行匹配到的所有 bin 替换为 BIN (其中 g 代表 global 即全局之意):
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed 's/bin/BIN/g' sed_demo.txt | nl
     1  root:x:0:0:root:/root:/BIN/bash
     2  BIN:x:1:1:BIN:/BIN:/sBIN/nologin
     3  daemon:x:2:2:daemon:/sBIN:/sBIN/nologin
     4  adm:x:3:4:adm:/var/adm:/sBIN/nologin
     5  lp:x:4:7:lp:/var/spool/lpd:/sBIN/nologin
  • 每行第2次匹配的 bin 替换为 BIN
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed 's/bin/BIN/2' sed_demo.txt | nl
     1  root:x:0:0:root:/root:/bin/bash
     2  bin:x:1:1:BIN:/bin:/sbin/nologin
     3  daemon:x:2:2:daemon:/sbin:/sBIN/nologin
     4  adm:x:3:4:adm:/var/adm:/sbin/nologin
     5  lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
  • 每行第2次出现到该行结束所有的 bin 替换为 BIN
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed 's/bin/BIN/2g' sed_demo.txt | nl
     1  root:x:0:0:root:/root:/bin/bash
     2  bin:x:1:1:BIN:/BIN:/sBIN/nologin
     3  daemon:x:2:2:daemon:/sbin:/sBIN/nologin
     4  adm:x:3:4:adm:/var/adm:/sbin/nologin
     5  lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
  • 在每一行的行首插入符号两个制表符:
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed 's/^/\t\t/g' sed_demo.txt 
                root:x:0:0:root:/root:/bin/bash
                bin:x:1:1:bin:/bin:/sbin/nologin
                daemon:x:2:2:daemon:/sbin:/sbin/nologin
                adm:x:3:4:adm:/var/adm:/sbin/nologin
                lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
  • 在每一行的行尾插入-------
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed 's/$/-------/g' sed_demo.txt 
root:x:0:0:root:/root:/bin/bash-------
bin:x:1:1:bin:/bin:/sbin/nologin-------
daemon:x:2:2:daemon:/sbin:/sbin/nologin-------
adm:x:3:4:adm:/var/adm:/sbin/nologin-------
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin-------
  • 将2-3行的 sbin 替换为 SBIN ,同时将第3行至最后一行的 nologin 替换为 NOLOGIN
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed '2,3s/sbin/SBIN/g; 3,$s/nologin/NOLOGIN/g' sed_demo.txt 
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/SBIN/nologin
daemon:x:2:2:daemon:/SBIN:/SBIN/NOLOGIN
adm:x:3:4:adm:/var/adm:/sbin/NOLOGIN
lp:x:4:7:lp:/var/spool/lpd:/sbin/NOLOGIN

替换行子命令 c

  • 将1-3行替换为 /etc/passwd
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed '1,3c /etc/passwd' sed_demo.txt 
/etc/passwd
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

删除行子命令 d

  • 删除1-3行:
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed '1,3d' sed_demo.txt 
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

设置行号子命令 =

  • 仅为第1-2行设置行号:
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed '1,2=' sed_demo.txt 
1
root:x:0:0:root:/root:/bin/bash
2
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

读取当前行的同时读取其下一行子命令 N

其实就是将当前行的下一行内容也读进缓存区,一起做匹配和修改,需要注意的是:当前行的 \n 仍然保留。

[root@iZbp142l91zbbe0hqz89bgZ ~]# sed '=' sed_demo.txt | sed 'N;s/\n/\t/'
1       root:x:0:0:root:/root:/bin/bash
2       bin:x:1:1:bin:/bin:/sbin/nologin
3       daemon:x:2:2:daemon:/sbin:/sbin/nologin
4       adm:x:3:4:adm:/var/adm:/sbin/nologin
5       lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

为便于理解上述命令,我们先看一下单独运行 sed '=' sed_demo.txt 的效果:

[root@iZbp1gjysfmcbcojeshiw7Z ~]# sed '=' sed_demo.txt 
1
root:x:0:0:root:/root:/bin/bash
2
bin:x:1:1:bin:/bin:/sbin/nologin
3
daemon:x:2:2:daemon:/sbin:/sbin/nologin
4
adm:x:3:4:adm:/var/adm:/sbin/nologin
5
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

即行号会插入在在每一行的前一行,而后 sed 'N;s/\n/\t/' 是:

  • 先将行号和其下一行一起读取;
  • 然后将换行符(\n)替换为制表符(\t)。

忽略大小写子命令 i

  • iI 均替换为 --I--
# 仅替换每一行的第一个i或I
[root@iZbp1gjysfmcbcojeshiw7Z ~]# sed 's/nologin/NOLOGIN/' sed_demo.txt | nl | sed 's/i/--I--/i'
     1  root:x:0:0:root:/root:/b--I--n/bash
     2  b--I--n:x:1:1:bin:/bin:/sbin/NOLOGIN
     3  daemon:x:2:2:daemon:/sb--I--n:/sbin/NOLOGIN
     4  adm:x:3:4:adm:/var/adm:/sb--I--n/NOLOGIN
     5  lp:x:4:7:lp:/var/spool/lpd:/sb--I--n/NOLOGIN
# 替换
[root@iZbp1gjysfmcbcojeshiw7Z ~]# sed 's/nologin/NOLOGIN/' sed_demo.txt | nl | sed 's/i/--I--/gi'
     1  root:x:0:0:root:/root:/b--I--n/bash
     2  b--I--n:x:1:1:b--I--n:/b--I--n:/sb--I--n/NOLOG--I--N
     3  daemon:x:2:2:daemon:/sb--I--n:/sb--I--n/NOLOG--I--N
     4  adm:x:3:4:adm:/var/adm:/sb--I--n/NOLOG--I--N
     5  lp:x:4:7:lp:/var/spool/lpd:/sb--I--n/NOLOG--I--N

基本选项

多个命令连接 -e

上述命令 sed '2,3s/sbin/SBIN/g; 3,$s/nologin/NOLOGIN/g' sed_demo.txt 等价于下列命令:

[root@iZbp142l91zbbe0hqz89bgZ ~]# sed -e '2,3s/sbin/SBIN/g' -e '3,$s/nologin/NOLOGIN/g' sed_demo.txt 
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/SBIN/nologin
daemon:x:2:2:daemon:/SBIN:/SBIN/NOLOGIN
adm:x:3:4:adm:/var/adm:/sbin/NOLOGIN
lp:x:4:7:lp:/var/spool/lpd:/sbin/NOLOGIN

关闭打印模式 -n

下面命令仅打印出了发生替换的行 (关闭每行都打印的同时,指定 p 来打印仅发生替换的行):

[root@iZbp142l91zbbe0hqz89bgZ ~]# sed -n 's/nologin/NOLOGIN/p' sed_demo.txt | nl
     1  bin:x:1:1:bin:/bin:/sbin/NOLOGIN
     2  daemon:x:2:2:daemon:/sbin:/sbin/NOLOGIN
     3  adm:x:3:4:adm:/var/adm:/sbin/NOLOGIN
     4  lp:x:4:7:lp:/var/spool/lpd:/sbin/NOLOGIN

修改源文件 -i

需要注意的是:至此, sed 修改匹配到的内容后,默认不会将修改保存到原文件,而是直接输出修改后模式空间(可理解为缓存)的内容到STDOUT,如果要修改原文件需要指定 -i 选项。

支持扩展正则表达式 -r-e

  • 删除文件每行的第二个字符:
[root@iZbp1gjysfmcbcojeshiw7Z ~]# sed -r 's/(.)(.)(.*)$/\1\3/' sed_demo.txt 
rot:x:0:0:root:/root:/bin/bash
bn:x:1:1:bin:/bin:/sbin/nologin
demon:x:2:2:daemon:/sbin:/sbin/nologin
am:x:3:4:adm:/var/adm:/sbin/nologin
l:x:4:7:lp:/var/spool/lpd:/sbin/nologin
  • 交换每行的第一个字符和第二个字符:
[root@iZbp1gjysfmcbcojeshiw7Z ~]# sed -r 's/(.)(.)(.*)$/\2\1\3/' sed_demo.txt 
orot:x:0:0:root:/root:/bin/bash
ibn:x:1:1:bin:/bin:/sbin/nologin
ademon:x:2:2:daemon:/sbin:/sbin/nologin
dam:x:3:4:adm:/var/adm:/sbin/nologin
pl:x:4:7:lp:/var/spool/lpd:/sbin/nologin

特殊变量

  • 使用变量 & 可以代表匹配出的结果:
[root@iZbp142l91zbbe0hqz89bgZ ~]# sed 's/nologin/"&"/g' sed_demo.txt 
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/"nologin"
daemon:x:2:2:daemon:/sbin:/sbin/"nologin"
adm:x:3:4:adm:/var/adm:/sbin/"nologin"
lp:x:4:7:lp:/var/spool/lpd:/sbin/"nologin"

使用正则

  • 去除下面文字中HTML标签(其中 [^>]* 表示 '>' 的字符出现0次或多次)
[root@iZbp142l91zbbe0hqz89bgZ ~]# echo '<b>This</b> is what <span style="x">I</span> meant' | sed 's/<[^>]*>//g'
This is what I meant

下面的正则中:

  • \([^,]\) 中的 \ 表示指定括号为逃逸字符,^含义为
  • \1\2 分别表示使用分组。
[root@iZbp142l91zbbe0hqz89bgZ ~]# echo "hello,123,world" | sed 's/\([^,]\),.*,\(.*\)/\1=\2/'
hello=world
sed -i 's/upjas.bind.address=19.20.11.134/upjas.bind.address='`hostname -i`'/g' upjas_setenv.properties

五、awk命令

本部分主要参考下列文章,仅记录以供个人学习参考:

1. awk简介

上面介绍的sed可以实现非交互式的字符串替换,grep能够实现有效的过滤功能。与两者相比,awk是一款强大的文本分析工具,尤其擅长对日志、csv文件等格式化数据进行分析并生成报告。

该命令之所以叫awk是因为其取了三位创始人Alfred AhoPeter WeinbergerBrian Kernighan的姓氏首字符。

2. awk工作原理

awk的命令格式如下:

awk 'BEGIN{ commands } pattern{ commands } END{ commands }'

基于上述命令格式,awk的工作流程可分为三个部分:

  • 读输入文件之前执行的代码段(由BEGIN关键字标识);
  • 主循环执行输入文件的代码段;
  • 读输入文件之后的代码段(由END关键字标识)。

下面的流程图详细描述出了awk的工作流程:

在这里插入图片描述

  1. 通过关键字BEGIN执行BEGIN块的内容,即BEGIN后花括号{}的内容;
  2. 完成BEGIN块的执行,开始执行BODY块;
  3. 读入由\n换行符分割的记录,一条记录即为一行;
  4. 将记录按指定的域分隔符划分为域(相当于数据库的字段取值),其中$0表示所有域(即一行内容),$1表示第一个域,$n表示第n个域;
  5. 依次执行各BODY块,pattern部分匹配该行内容成功后,才会执行awk命令中commands部分的内容;
  6. 循环读取并执行各行直到文件结束,完成BODY块执行;
  7. 开始END块执行,END块可以输出最终结果。

开始块BEGIN

开始块的语法格式如下:

BEGIN {awk-commands}

开始块就是在程序启动的时候执行的代码部分,并且它在整个过程中只执行一次。一般情况下,我们可以在开始块中初始化一些变量。

BEGINawk的关键字,因此它必须是大写的。

注意:开始块部分是可选的,你的程序可以没有开始块部分。

主体块BODY

主体部分的语法格式如下:

/pattern/ {awk-commands}

对于每一个输入的行都会执行一次主体部分的命令。

默认情况下,对于输入的每一行,awk都会执行命令。但是,我们可以将其限定在指定的模式中。

注意:在主体块部分没有关键字存在。

结束块END

结束块的语法格式如下:

END {awk-commands}

结束块是在程序结束时执行的代码。END也是awk的关键字,它也必须大写。

注意:与开始块相似,结束块也是可选的。

3. awk实战演示

为了演示方便,我们使用下列命令,先创建一个用于演示用的TXT文档:

[root@iZbp1ewxwj89u3zpajnxz4Z sys]# ls -l > /root/awk_demo.txt
[root@iZbp1ewxwj89u3zpajnxz4Z sys]# cd /root
[root@iZbp1ewxwj89u3zpajnxz4Z ~]# cat awk_demo.txt 
drwxr-xr-x   2 root root 0 May 21  2021 block
drwxr-xr-x  35 root root 0 May 21  2021 bus
drwxr-xr-x  54 root root 0 May 21  2021 class
drwxr-xr-x   4 root root 0 May 21  2021 dev
drwxr-xr-x  15 root root 0 May 21  2021 devices
drwxr-xr-x   6 root root 0 May 21  2021 firmware
drwxr-xr-x   6 root root 0 May 21  2021 fs
drwxr-xr-x   2 root root 0 May 21 16:37 hypervisor
drwxr-xr-x  14 root root 0 May 21  2021 kernel
drwxr-xr-x 114 root root 0 May 21  2021 module
drwxr-xr-x   2 root root 0 May 21 16:37 power

输出

输出指定列

# 输出文档awk_demo.txt的第1,4,8列
[root@iZbp1ewxwj89u3zpajnxz4Z ~]# awk '{print $1,$4,$8}' awk_demo.txt 
drwxr-xr-x root 2021
drwxr-xr-x root 2021
drwxr-xr-x root 2021
drwxr-xr-x root 2021
drwxr-xr-x root 2021
drwxr-xr-x root 2021
drwxr-xr-x root 2021
drwxr-xr-x root 16:37
drwxr-xr-x root 2021
drwxr-xr-x root 2021
drwxr-xr-x root 16:37

大括号里边的就是awkcommand只能被单引号包含,其中,$1..$N表示第几列,$0表示整个的行内容。

格式化输出

awk还支持类型C语言printf函数的格式化输出:

[root@iZbp1ewxwj89u3zpajnxz4Z ~]# awk '{printf "%-20s %-8s %-10s %-8s\n",$1,$2,$3,$4}' awk_demo.txt 
drwxr-xr-x           2        root       root    
drwxr-xr-x           35       root       root    
drwxr-xr-x           54       root       root    
drwxr-xr-x           4        root       root    
drwxr-xr-x           15       root       root    
drwxr-xr-x           6        root       root    
drwxr-xr-x           6        root       root    
drwxr-xr-x           2        root       root    
drwxr-xr-x           14       root       root    
drwxr-xr-x           114      root       root    
drwxr-xr-x           2        root       root

其中,和C语言类型,%s表示字符串占位符,-20表示列宽度为20且左对齐。

输出过滤的行

仅输出第3列为root且第8列为16:37的行:

[root@iZbp1ewxwj89u3zpajnxz4Z ~]# awk '$3 == "root" && $8 == "16:37"  {print $0}' awk_demo.txt 
drwxr-xr-x   2 root root 0 May 21 16:37 hypervisor
drwxr-xr-x   2 root root 0 May 21 16:37 power

awk支持各种比较运算符号!=><>=<=,其中$0表示整行的所有内容。

使用内置变量

字段数NF

如下,awk在读取文件时,按行读取,每一行的字段数(列数),赋值给内置变量NF,打印出来的就是每行的字段总数:

[root@iZbp15brp59m56cywazt3yZ ~]# awk '{print "字段数:" NF}' awk_demo.txt 
字段数:9
字段数:9
字段数:9
字段数:9
字段数:9
字段数:9
字段数:9
字段数:9
字段数:9
字段数:9
字段数:9

如果只需要最后一列的数据,由于每一行的列数可能不一,最后一列无法指定固定的列数,此时就可以使用NF来表示列数,'{print $NF}'表示打印出等于总列数的那一列的数据。

[root@iZbp15brp59m56cywazt3yZ ~]# awk '{print $NF}' awk_demo.txt 
block
bus
class
dev
devices
firmware
fs
hypervisor
kernel
module
power

记录数NRFNR

如下,打印出读取文件的行数,因为是按行读取,在应用场景中,行数可以等同于行号,用来输出对应行的行号:

[root@iZbp15brp59m56cywazt3yZ ~]# awk '{print "行号为:" NR}' awk_demo.txt 
行号为:1
行号为:2
行号为:3
行号为:4
行号为:5
行号为:6
行号为:7
行号为:8
行号为:9
行号为:10
行号为:11

NR还可以用作判断输出,如下简单例子:

# 过滤第8列为"10:52"且行号大于9的记录,打印时仅打印行号、每条记录第一个字段、每条记录最后一个字段
[root@iZbp15brp59m56cywazt3yZ ~]# awk '$8 == "10:52" {if(NR>9)print NR,$1,$NF}' awk_demo.txt 
11 drwxr-xr-x power

FNR也是读取文件的行数,但是和NR不同的是,当读取的文件有两个或两个以上时,NR读取完一个文件,行数继续增加,而FNR重新从1开始记录:

[root@iZbp15brp59m56cywazt3yZ ~]# awk '{print "NR:" NR "\t\t" "FNR:" FNR}' awk_demo.txt awk_demo.txt 
NR:1            FNR:1
NR:2            FNR:2
NR:3            FNR:3
NR:4            FNR:4
NR:5            FNR:5
NR:6            FNR:6
NR:7            FNR:7
NR:8            FNR:8
NR:9            FNR:9
NR:10           FNR:10
NR:11           FNR:11
NR:12           FNR:1
NR:13           FNR:2
NR:14           FNR:3
NR:15           FNR:4
NR:16           FNR:5
NR:17           FNR:6
NR:18           FNR:7
NR:19           FNR:8
NR:20           FNR:9
NR:21           FNR:10
NR:22           FNR:11

输入字段分隔符FS

awk中默认分隔一行各个字段的符号为空格,而实际的文本数据并不总是以空格为分隔符,我们可以通过FS变量指定分隔符,为了后续演示方便,下面先创建一个以:分隔的文档:

[root@iZbp15brp59m56cywazt3yZ ~]# awk '{ if (NR < 6) print $0 }' /etc/passwd > /root/awk_sep_demo.txt
[root@iZbp15brp59m56cywazt3yZ ~]# cat awk_sep_demo.txt 
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

如下所示,因为awk_sep_demo.txt里面字母间是以:来分割的,所以:

  • 第一种写法,由于没有指定FSawk默认是以空格来分割,每一行所有内容都当作$1来显示;
  • 第二个指定FS:分割,所以能按照我们期望的方式来打印。
[root@iZbp15brp59m56cywazt3yZ ~]# awk '{print $1}' awk_sep_demo.txt 
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
[root@iZbp15brp59m56cywazt3yZ ~]# awk 'BEGIN { FS = ":" } { print $1 }' awk_sep_demo.txt 
root
bin
daemon
adm
lp

实际上,通过选项-F也可以实现类似的功能:

[root@iZbp15brp59m56cywazt3yZ ~]# awk -F ':' '{ print $1 }' awk_sep_demo.txt 
root
bin
daemon
adm
lp

再推广一下,如果想要一次性指定多个分隔符,如:,;等,可以使用类似-F '[;:,]'的格式。

输出字段分隔符OFS

输出字段分割符,默认为空格,实际需求可能要求输出是以若干个制表符分割,可以使用OFS进行格式化输出:

[root@iZbp15brp59m56cywazt3yZ ~]# awk 'BEGIN { FS = ":"; OFS = "\t\t\t" } { print $1, $2, $3 }' awk_sep_demo.txt 
root                    x                       0
bin                     x                       1
daemon                  x                       2
adm                     x                       3
lp                      x                       4

输入行分隔符RS

输入行分隔符RS,用于判断输入部分的行的起始位置,默认是换行符,下面将其改为:

[root@iZbp15brp59m56cywazt3yZ ~]# awk '{if(NR == 1) print $0}' awk_sep_demo.txt | awk 'BEGIN { RS = ":" } { print $0 }'
root
x
0
0
root
/root
/bin/bash

你可能注意到/bin/bash之后有一空行,原因在于:通过awk '{if(NR == 1) print $0}' awk_sep_demo.txt获得的是文件中的第一行,该行最后一个看不见的换行符'\n'

输出行分割符ORS

输出行分割符,默认的是换行符,它的机制和OFS机制一样,对输出格式有要求时,可以进行格式化输出:

[root@iZbp15brp59m56cywazt3yZ ~]# awk 'BEGIN { ORS = "\t\t" } { print }' awk_sep_demo.txt 
root:x:0:0:root:/root:/bin/bash         bin:x:1:1:bin:/bin:/sbin/nologin                daemon:x:2:2:daemon:/sbin:/sbin/nologin         adm:x:3:4:adm:/var/adm:/sbin/nologin          lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin                

简单数据统计

如下,下面的命令统计当前目录下,所有文件大小的总和:

[root@iZbp15brp59m56cywazt3yZ ~]# ls -l
total 20
-rw-r--r-- 1 root root 511 May 26 11:07 awk_demo.txt
-rw-r--r-- 1 root root 183 May 26 14:50 awk_sep_demo.txt
-rw-r--r-- 1 root root  33 May 26 16:37 bin.txt
-rw-r--r-- 1 root root 118 May 26 16:37 others.txt
-rw-r--r-- 1 root root  32 May 26 16:37 root.txt
[root@iZbp15brp59m56cywazt3yZ ~]# ls -l | awk '{ sum += $5 } END { print sum }'
877

第5列表示文件大小,每读取一行就会将该文件大小计算到sum变量中,在最后END阶段打印出sum,也就是所有文件的大小总和。

4. awk进阶

数组

awk数组

内置函数

awk内置函数

awk内置支持一系列函数,其中length计算字符串长度,toupper函数转换字符串为大写。

[root@iZbp15brp59m56cywazt3yZ ~]# awk 'BEGIN { FS = ":" } { if (length($1) == 3) print $1, "\t", toupper($1) }' awk_sep_demo.txt 
bin      BIN
adm      ADM

条件语句与循环

条件

awk条件语句与循环

利用NRFNR这两个内置变量,使用其条件语句,有一个有趣的应用,即比较两个文件awk_demo.txtawk_sep_demo.txt是否一致,以awk_demo.txt作为参考,不一致的输出行号和该行信息:

[root@iZbp15brp59m56cywazt3yZ ~]# awk '{print "NR:" NR "\t\t" "FNR:" FNR}' awk_demo.txt awk_sep_demo.txt 
NR:1            FNR:1
NR:2            FNR:2
NR:3            FNR:3
NR:4            FNR:4
NR:5            FNR:5
NR:6            FNR:6
NR:7            FNR:7
NR:8            FNR:8
NR:9            FNR:9
NR:10           FNR:10
NR:11           FNR:11
NR:12           FNR:1
NR:13           FNR:2
NR:14           FNR:3
NR:15           FNR:4
NR:16           FNR:5
[root@iZbp15brp59m56cywazt3yZ ~]# awk '{ if (NR == FNR) { array[NR] = $0 } else { if (array[FNR] != $0) { print FNR, array[FNR] } } }' awk_demo.txt awk_sep_demo.txt 
1 drwxr-xr-x   2 root root 0 May 26  2021 block
2 drwxr-xr-x  35 root root 0 May 26  2021 bus
3 drwxr-xr-x  54 root root 0 May 26  2021 class
4 drwxr-xr-x   4 root root 0 May 26  2021 dev
5 drwxr-xr-x  15 root root 0 May 26  2021 devices

上述awk语句的含义为:

  • 当读取第一个文件awk_demo.txt的时候NRFNR都是从1开始计数,这时NR == FNR将该行赋值给数组;
  • NR != FNR此时表示已读取到第二个文件,将数组中的内容和当前行$0进行比较,如果不相同,则输出行号和该行信息。

下面的例子利用条件判断,根据每一条记录所属的用户,分别将各条记录输出至文件root.txtbin.txt以及others.txt

[root@iZbp15brp59m56cywazt3yZ ~]# cat awk_sep_demo.txt 
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
[root@iZbp15brp59m56cywazt3yZ ~]# awk 'BEGIN { FS = ":" } { if($1 == "root") print > "root.txt"; \
else if($1 == "bin") print > "bin.txt"; \
else print > "others.txt" }' awk_sep_demo.txt
[root@iZbp15brp59m56cywazt3yZ ~]# cat root.txt 
root:x:0:0:root:/root:/bin/bash
[root@iZbp15brp59m56cywazt3yZ ~]# cat bin.txt 
bin:x:1:1:bin:/bin:/sbin/nologin
[root@iZbp15brp59m56cywazt3yZ ~]# cat others.txt 
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

循环

下面的例子用到了数组for循环,值得一提的是,awk的数组可以理解为字典或Mapkey可以是数值和字符串,这种数据类型在平时很常用。

[root@iZbp15brp59m56cywazt3yZ ~]# ps -aux | awk 'BEGIN { OFS = "\t\t\t" } { if(NR != 1) array[$1] += $6 } END { for(i in array) print i, array[i]}'
systemd+                        8876
chrony                  3864
polkitd                 21816
dbus                    5484
rngd                    6488
libstor+                        1860
root                    283532

需要注意的是,由于第一行为表头,需要通过条件判断if(NR != 1)将其排除。

用户自定义函数

awk用户自定义函数

awk脚本编写与运行

为了从整体上理解awk工作机制,我们再来看一个综合的示例,假设有一个学生成绩单:

[root@iZbp15brp59m56cywazt3yZ ~]# cat scores.txt 
Marry   2143    78      84      77
Jack    2321    66      78      45
Tom     2122    48      77      71
Mike    2537    87      97      95
Bob     2415    40      57      62

由于此示例程序稍显复杂,在命令行上不易读,另外呢,也想通过此案例介绍另外一种 awk的执行方式,我们的awk脚本如下:

[root@iZbp15brp59m56cywazt3yZ ~]# cat cal_scores.awk 
#!/bin/awk -f
BEGIN {
        math = 0
        English = 0
        computer = 0
        printf "NAME    NO.     MATH    ENGLISH         COMPUTER        TOTAL\n"
        printf "-------------------------------------------------------------\n"
}
{
        math += $3
        English += $4
        computer += $5
        print $1, "\t", $2, "\t", $3, "\t", $4, "\t\t", $5, "\t\t", $3 + $4 + $5
}
END {
        printf "-------------------------------------------------------------\n"
        print "TOTAL:", "\t\t", math, "\t", English, "\t\t", computer
        print "AVERAGE:", "\t", math / NR, "\t", English / NR, "\t\t", computer / NR
}

执行awk结果如下:

[root@iZbp15brp59m56cywazt3yZ ~]# awk -f cal_scores.awk scores.txt 
NAME    NO.     MATH    ENGLISH         COMPUTER        TOTAL
-------------------------------------------------------------
Marry    2143    78      84              77              239
Jack     2321    66      78              45              189
Tom      2122    48      77              71              196
Mike     2537    87      97              95              279
Bob      2415    40      57              62              159
-------------------------------------------------------------
TOTAL:           319     393             350
AVERAGE:         63.8    78.6            70

我们可以将复杂的 awk 语句写入脚本文件 cal_scores.awk,然后通过 -f 选项指定从脚本文件执行。

  • BEGIN 阶段,我们初始化了相关变量,并打印了表头的格式;
  • body 阶段,我们读取每一行数据,计算该学科和该同学的总成绩;
  • END 阶段,我们先打印了表尾的格式,并打印总成绩,以及计算了平均值。