# 文本处理

# 查看文本

有多种命令可以查看文件的内容。

  • 有的文件是采用二进制格式存储的,不能直接阅读。
  • 有的文件是采用 ASCII 码等编码格式存储的,称为文本文件,可以直接阅读。
    • 如果查看文本文件时出现乱码,说明解码时使用的编码格式不对。

本文中,大部分命令的输入参数都声明为 [file] 。如果不指定文件参数 file ,或者指定 - 作为 file ,该命令就会读取 stdin 作为输入。例如:ps | wc -l

# cat

$ cat [file]    # 显示文件的内容
      -n        # 同时显示行号
      -b        # 空白行不记行号
      -E        # 在每行的末尾显示 $
      -T        # 将 Tab 符显示成 ^I
      -v        # 显示不会 print 的特殊字符
  • 不适合查看内容较多的文件,否则会显示好几页终端。
  • 例:
    cat f1 > f2  # 拷贝文件 f1 的内容到文件 f2 中(相当于只 cp 内容)
    

# tac

$ tac [file]     # 从最后一行开始倒序显示文件的内容(与 cat 命令相反)
$ head [file]    # 显示文件开头的 10 行
       -n        # 只显示 n 行
       -c n      # 只显示 n 个字节,可以使用单位 K、M、G、T 等

# tail

$ tail [file]    # 显示文件末尾的 10 行
       -n        # 只显示 n 行
       -c n      # 只显示 n 个字节
       -f n      # 跟踪显示文件的末尾,当末尾增加内容时就显示出来(这会阻塞前台)

# more

$ more <file>    # 打开阅读器,显示文件的全部内容(只能向前翻页)

# less

$ less <file>    # 打开阅读器,显示文件的全部内容(可以前后翻页、查找)
       -N        # 显示行号
  • 该阅读器的控制方法与 vim 相似,只是要按 q 键退出。

# 文本排版

# column

$ column [file]...
         -t          # 将每行文本分割成多个字段,从而将输入的多行文本转换成一个列表(会自动上下对齐)
         -s " "      # 指定输入文本的字段分隔符(默认是连续的空白字符)
         -o "  "     # 指定输出列表的字段分隔符(默认是至少两个空格)
  • 例:
    [root@CentOS ~]# echo $PATH
    /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    [root@CentOS ~]# echo $PATH | column -t -s :
    /usr/local/sbin  /usr/local/bin  /usr/sbin  /usr/bin  /sbin  /bin
    [root@CentOS ~]# echo $PATH | column -t -s : -o ', '
    /usr/local/sbin, /usr/local/bin, /usr/sbin, /usr/bin, /sbin, /bin
    

# sort

$ sort [file]...    # 将每行文本排序之后输出
       -k 1         # 按每行的第 1 个字段进行排序
       -t " "       # 指定每行的字段分隔符
       -r           # 反序排列
  • 例:
    [root@CentOS ~]# ls -al | sort -k 9
    total 216
    dr-xr-x---. 22 root root  4096 Dec  6 15:54 .
    dr-xr-xr-x. 19 root root  4096 Dec  9 10:00 ..
    -rw-------   1 root root 41827 Dec  9 09:59 .bash_history
    -rw-r--r--.  1 root root    18 Dec 29  2013 .bash_logout
    -rw-r--r--.  1 root root   176 Dec 29  2013 .bash_profile
    

# jq

$ jq <filter> [file...]   # 读取文本,按 JSON 格式解析,然后输出
  • 安装: yum install jq
  • 例:
    jq . test.json                      # filter 为 . ,这会输出整个 JSON 对象
    cat test.json | jq .links[0]        # 只输出 filter 指定的元素
    cat test.json | jq ,.name,.status   # filter 可以指定多个元素
    cat test.json | jq [.name, .status]                   # 输出为 array 类型
    cat test.json | jq '{name: .name, status: .status}'   # 输出为 object 类型
    cat test.json | jq '"name=\(.name)"'                  # 输出为 string 类型,可以用 \(x) 的格式插入值
    cat test.json | jq 'del(.name, .status)'              # 删除一些字段
    
  • 还可用 Python 进行格式化:
    python -m json.tool <file>
    

# yq

$ yq <expression> [file...]
    -I <int>    # --indent ,设置输出时的缩进,默认为 2
    -i          # --inplace ,将输出保存到源文件中。此时不支持读取 stdin 作为输入
  • yq 命令的用法与 jsq 类似,但支持解析 YAML、JSON、XML 文本格式。
  • 安装:
    wget https://github.com/mikefarah/yq/releases/download/v4.19.1/yq_linux_amd64 -O /usr/bin/yq
    chmod +x /usr/bin/yq
    
  • 例:
    yq '.a[0].b' test.yaml              # 输出指定字段
    cat test.yaml | yq -                # 读取 stdin
    yq -i '.a="hello"' test.yaml        # 替换一个字段的值
    yq '.a[]' test.yaml -s '.name'      # 将 list 元素分别保存到不同文件中,文件名引用其中的 .name 字段
    
  • yq 命令不支持通过 $ 读取 shell 环境变量。而要使用以下语法:
    yq -i ".a=env(PWD)"  test.yaml      # 引用 shell 环境变量,自动保存为字符串、数值、数组等数据类型
    yq -i ".a=strenv(PWD)"  test.yaml   # 引用 shell 环境变量,总是保存为字符串类型
    
  • 如果目标文件中包含多个用 --- 分隔的 YAML 文档,则会被 yq 分别处理。

# 统计分析

# wc

$ wc [file]...
     -l        # 统计行数
     -c        # 统计字节数
     -m        # 统计字符数

# uniq

$ uniq [file]...  # 去掉文本中重复出现的行,再显示
      -c          # 统计每行重复出现的次数,显示在每行左侧
      -d          # 仅显示重复出现的行
      -u          # --unique ,仅显示没有重复的行
      -i          # --ignore-case ,忽略大小写
  • uniq 识别重复的行时,这些行必须相邻,因此通常先用 sort 将文本排序,再用 uniq 去重。如下:
    [root@CentOS ~]# cat f1                   # 查看文件的原内容
    Hello World
    Hello
    Hello World
    Hello
    [root@CentOS ~]# uniq f1                  # 此时重复的行没有相邻,因此 uniq 不能识别
    Hello World
    Hello
    Hello World
    Hello
    [root@CentOS ~]# cat f1 | sort            # 将文本排序
    Hello
    Hello
    Hello World
    Hello World
    [root@CentOS ~]# cat f1 | sort | uniq     # 将文本排序后去重
    Hello
    Hello World
    [root@CentOS ~]# cat f1 | sort | uniq -c  # 统计每行重复出现的次数
          2 Hello
          2 Hello World
    

# diff

$ diff
       <file1> <file2>  # 逐行比较两个文本文件的内容差异(如果没有差异,则显示为空)
            -q          # 安静模式(如果有差异,则不显示具体差异,只显示一行提示)
            -y          # 在屏幕左右侧分别显示两个文件的内容,类似 GUI
            -b          # 忽略空格(多个连续空格视作一个空格)
            -B          # 忽略空行
            -i          # 忽略大小写的差异
            -w          # 忽略所有空白字符
       <dir1> <dir2>    # 比较两个目录:先找出各自独有的文件,再比较同名文件的内容差异
            -r          # 递归比较(默认不会比较子目录)
  • 安装: yum install diffutils
  • 例:
    cat f2 | diff f1 -  # 通过 stdin 传入一个文件进行比较
    
    [root@CentOS ~]# diff -y f1 f2
    A                     |
    B                     |
    C                     |
    D                       D
                          > E
                          > F
                          > G
    [root@CentOS ~]# diff f1 f2
    1,3c1,3
    < A
    < B
    < C
    ---
    >
    >
    >
    4a5,7
    > E
    > F
    > G
    
    diff 的显示结果表示了从文件 1 到文件 2 的变化。
    • 例如,1,3c1,3 表示从文件 1 的第 1~3 行到文件 2 的第 1~3 行有变化。
    • a 表示 add ,c 表示 change ,d 表示 delete 。
    • --- 是两个文件内容的分隔线。

# hexdump

$ hexdump <file>...     # 将文件内容显示成十六进制值(两个字节为一组)
            -c          # 显示成 ASCII 码
            -C          # 显示成十六进制值 + ASCII 码(每个字节为一组)
            -n 4        # 最多显示 4 个字节
            -s  4       # 从第 4 个字节开始显示
  • 例:

    [root@CentOS ~]# echo 'Hello World! How are you?' > f1
    [root@CentOS ~]# hexdump f1
    0000000 6548 6c6c 206f 6f57 6c72 2164 4820 776f     # 第一排包含从地址 0000000 到 000000F 处的 16 个字节
    0000010 6120 6572 7920 756f 0a3f
    000001a                                             # 最后一排表示该文件的总字节数
    [root@CentOS ~]# hexdump -C f1
    00000000  48 65 6c 6c 6f 20 57 6f  72 6c 64 21 20 48 6f 77  |Hello World! How|
    00000010  20 61 72 65 20 79 6f 75  3f 0a                    | are you?.|
    000001a
    [root@CentOS ~]# hexdump -n 1 f1
    0000000 0048
    0000001
    [root@CentOS ~]# hexdump -n 2 f1
    0000000 6548
    0000002
    
    • 上例中,地址 0000000、0000001 处的字节内容分别为 48、65 ,对应的 ASCII 字符分别为 H、e 。
    • 显示 . 代表不能解码成 ASCII 码中的可显示字符。
  • xxd 命令与之类似:

    [root@CentOS ~]# echo Hello | xxd
    0000000: 4865 6c6c 6f0a                           Hello.
    

# 筛选文本

# grep

$ grep <pattern> [file]...  # 筛选出文件中匹配 pattern 的每行文本
       [-e pattern]...      # --regexp ,指定基本语法的正则表达式(这是默认类型)
       [-E pattern]...      # --extended-regexp ,指定扩展语法的正则表达式
       [-P pattern]...      # --perl-regexp ,指定 Perl 语法的正则表达式
       -i                   # --ignore-case ,忽略大小写
       -r                   # --recursive ,递归检索指定目录下的所有文件

       -c                   # --count ,只显示匹配的总行数
       -h                   # --no-filename ,输出匹配结果时不显示文件名。只读取一个文件或 stdin 时,默认启用该选项
       -o                   # --only-matching ,只显示每行中与 pattern 匹配的子字符串,而不是整行
       -q                   # --quit ,不显示匹配结果,只能只能根据命令返回码判断匹配是否成功
       -n                   # --line-number ,增加显示每行的行号
       -v                   # 反向匹配

       -l                   # 如果文件内容匹配,则只显示文件名
       -L                   # 只显示不匹配的文件名
  • 如果 grep 的匹配结果不为空,则返回码为 0 ,表示匹配成功(只要有一行文本匹配成功即可)。否则为非 0 ,表示匹配失败。
  • 例:
    grep -n root /etc/passwd
    grep -v '^$' /etc/passwd                  # 去掉空行
    grep README.md -e hello -e world          # 可以指定多个 pattern ,只要文本匹配任一 pattern 就会被筛选出来
    
    grep -cr DEBUG . | grep -v :0             # 递归检索指定目录下的所有文件
    find . -name *.py | xargs grep -c DEBUG   # 先找出一组文件,再筛选文本
    

# cut

$ cut [file]...
      -f 1-4        # 显示每行的第 1 至 4 个字段
      -f 1 -d ","   # 显示每行的第 1 个字段,以 , 作为字段分隔符
      -b "1 2 3"    # 显示每行的第几个字符

# awk

$ awk [option] [expression] [file]...
      -F ,                        # 设置字段分隔符(默认是一个或多个空格、Tab)

      # 筛选显示内容
      '{print $3}'                # 显示每行的第 3 个字段
      '{print NR, $(NF-1)}'       # 显示每行的行号(从 1 开始编号)、倒数第二个字段
      '$3="hello"; print'         # 设置第 3 个字段的值,然后打印全部内容
      'NR>1 && NR<=10'            # 显示行号在指定范围的行
      '/^\S+/ {print $2,$3}'      # 要求当前行匹配 pattern ,然后显示指定字段
      '$1=="root" {print $3}'     # 要求第一个字段等于指定字符串,才显示
      '/Hello/,/^\s\s/'           # 查找两个 pattern 匹配的行,显示它们之间的所有行。其中的逗号 , 是 awk 语法的分隔符

      # 进行运算
      '{sum+=$1} END {print sum / 1024}'  # 计算第一个字段的总和

      # 引用 shell 变量
      -v a=$A -v b=$B '{print $a}'
  • awk 的表达式要用单引号包住,避免转义。而正则表达式 pattern 要用 / / 包住。
  • awk 的内置变量:
    • $0 :当前行的全部字段。
    • $1 :当前行的第一个字段,以此类推。
      • 如果 $n 超过当前行的最大字段数,则获取的值为空。
    • NF :当前行的最大字段数。
    • NR :当前行数。
  • awk 命令不支持直接修改文件,可采用以下方式:
    awk '{$3=$2; print}' f1 > f2
    rm -f f1
    mv f2 f1
    

# 修改文本

# tr

$ tr <源字符集> <目标字符集>   # 替换文本中的指定字符
     -c                      # 反选源字符集(即选中其它字符)
     -d                      # 删除源字符集
  • tr 命令的输入不能是文件,只能是 stdin ,且输出到 stdout 。
  • 例:
    cat f1 | tr a-z A-Z      # 将小写字母换成大写字母
    

# sed

$ sed [expression] [file]...  # 读取文件内容,根据表达式进行修改,然后输出
      -e                      # 读取 stdin
      -i                      # 将输出保存到源文件中,采用覆盖式写入(默认是输出到 stdout)
      -n                      # --quiet ,取消默认输出,此时使用 p 等命令才会有输出
  • 表达式示例:

    # 处理指定行
    -n '1p'                  # 只显示第 1 行
    -n '1,5p'                # 只显示第 1~5 行
    '1,$d'                   # 删除第 1 行至最后一行(使用单引号作为定界符,避免将 $ 当做对变量取值),然后输出剩下的内容
    '1,4d;6d;$d'             # 删除第 1~4 行、第 6 行、最后一行(用 ; 分隔多个目标)
    '1i hello'               # 在第 1 行之前插入一行字符串
    '1a hello'               # 在第 1 行之后插入一行字符串
    'a  hello'               # 在每行之后插入一行字符串
    
    # 使用正则匹配
    -n '/hello/p'            # 找到正则匹配的每行并显示,相当于 grep 命令。如果不加 -n ,则会将原始文本也输出
    '/hello/d'               # 找到正则匹配的每行并删除
    
    # 使用正则替换
    's/源字符串/目标字符串/g'  # 替换字符串,源字符串采用基本正则语法
    's#^hello#hi#g'          # s 之后的第一个字符会被视作分隔符
    's#hello##g'             # 目标字符串为空,则会删除源字符串
    's#hello##2'             # 只替换第二次
    's#hello \(\w*\)#\1#g'   # 可以按 \1 的格式提取正则匹配的元素组
    
    • sed 正则表达式的特点:
      • 大部分元字符需要加上 \ 转义,除了 ^$.*[]
      • 支持 \w、\s 及相反字符集,不支持 \d 字符集。
  • 例:

    cat f1 | sed 's#hello##g' > f2  # 将修改结果保存到另一个文件
    sed -i 's#hello##g' f1          # 将修改结果保存到源文件中
    sed -i '0,/^hello/s##Hello#' f1 # 只替换第一个匹配的字符串
    
    [root@CentOS ~]# echo 'Hello World' | sed 's#Hello#Hi#g'
    Hi World
    [root@CentOS ~]# echo 'Hello World' | sed 's#Hello \(\w*\)#\1#g'
    World
    
    sed -i 's#first_line#Hello\
    World\
    first_line#g' f1      # 插入多行
    

# vi / vim

  • vi 是类 Unix 系统的内置文本编辑器,而 vim 是最流行的类 vi 编辑器,功能很多。

  • 同类软件:

    • nano :一个比 vi 功能更简单的文本编辑器,执行命令 nano 即可进入。
    • less :一个只读的文本阅读器。
  • 执行以下命令即可启动 vim 编辑器:

    $ vim [path]
    
    • 如果不指定 path ,则会打开一个空的编辑器界面。
    • 如果指定的 path 是一个文件,则对它进行编辑;如果该文件不存在,则可以在保存时创建它。
    • 如果指定的 path 是一个目录,则可以管理该目录下的文件。
    • vim 的命令支持组合,可以连续输入多条命令,组合它们的功能。
  • 用 vim 打开文本文件时,会将整个文件的内容加载到内存中,并扫描每行内容,进行语法突出显示。因此占用的内存比文件体积稍大,还占用一定 CPU 。

    • 而使用 less 时,只会将文件的部分内容加载到内存中,显示给用户看,因此占用的内存很少。

# 缓存文件

  • 用 vi/vim 打开一个文件时,会在其目录下生成一个缓存文件,用于缓存文件的最后修改状态,命名格式为 .{filename}.swp
    • 当用户修改文件并退出 vi/vim 时,会用缓存文件替换原文件,导致文件的 inode 变化。
  • 如果目录下存在缓存文件,则可能是因为:
    • 此时有其他用户正在用 vi/vim 打开该文件。
    • 上次修改了文件,但异常退出 vi/vim 。此时建议执行以下命令:
      vim -r .{filename}.swp  # 恢复到文件的最后修改状态
      :wq                     # 保存文件
      rm -f .{filename}.swp   # 删除缓存文件
      

# 命令模式

启动 vim 时,默认进入命令模式(Command mode):

  • 此时不能编辑文本,只能输入键盘上的某些字符作为命令(区分大小写)。
  • 文本末尾显示的 ~ 表示空行,并不是实际存在的字符。
  • vim 的命令支持组合,可以连续输入多条命令,组合它们的功能。
    • 比如输入一个数字 n 之后再输入 dd ,会连续剪贴 n 行。
  • 关于切换模式的命令:
    • i/a :进入插入模式。
    • o :在光标下方插入一行并进入插入模式。
    • r :替换光标所在的那个字符。
    • R :进入替换模式。
    • 输入以 :/? 开头的字符串会进入底线命令模式。
    • 进入其它模式之后,按 Esc 即可退出到命令模式。
  • 关于移动光标的命令:
    • PageUpPageDown :向上或下翻页。
    • kjhl :将光标向上、下、左、右移动一格。
    • Space :将光标后移一格。
    • Enter :将光标下移一行。
    • gg :将光标移动到第一行。
    • G :将光标移动到最后一行。
    • 输入 /word(或 ?word)会向下(或向上)查找 word 字符串,然后输入 n(或 N)会切换到下一个(或上一个)匹配结果。
  • 关于撤销的命令:
    • u :撤销上一次操作。
    • Ctrl + r :重做被撤销的操作。
    • . :重做上一次操作(对 u 和 Ctrl + r 无效)。
  • 关于复制粘贴的命令:
    • yy :复制光标所在的当前行。
    • xX :删除光标之后或之前的一个字符。
    • dd :剪贴当前行。
    • pP :粘贴到下一行或上一行。
    • v :开始选中,此时光标移动过的区域都会被反白选中,然后输入 d 或 y 即可删除或复制。
    • Ctrl + v :开始矩形选中。

# 底线命令模式

底线命令模式(Last line mode):

  • 此时输入的字符会显示在下方的命令行中。

  • 常用命令:

    • :wq :保存并退出(输入 w 表示保存,输入 q 表示退出)
    • :wq! :强制保存再退出(输入!表示强制执行操作)
    • :wq [文件名] :保存为指定文件。
    • :s/源字符串/目标字符串/g :在当前行替换字符串。(目标字符串为空的话就是删除)
    • :%s/源字符串/目标字符串/g :在每一行替换字符串。
    • :set nu :显示行号。
    • /str :向下查找字符串。然后输入 n(或 N)会切换到下一个(或上一个)匹配结果。
    • ?str :向上查找字符串。
  • 可以在 /etc/vimrc~/.vimrc 文件永久保存 vim 的配置。常见的配置如下:

    set encoding=utf-8  " 设置编码格式
    set number          " 显示行号
    set nonumber        " 不显示行号
    set ignorecase      " 搜索时不区分大小写
    set paste           " 进入粘贴模式,使粘贴的文本内容会原样地输入 vim
    set nopaste         " 退出粘贴模式
    set tabstop=4       " 每个制表符 \t 显示的宽度
    set expandtab       " 按下 Tab 键时,输入空格而不是制表符
    
    • 用双引号 " 声明单行注释。

# iconv

iconv [file]        # 转换文本文件的编码格式(默认输出到 stdout)
      -f utf-8      # 源文件的编码格式
      -t gbk        # 要转换成的编码格式
      -c            # 忽略转换失败的字符
  • 例:批量转换文件的编码格式
    file_list=`find . -name "*.txt"`
    from_encoding='utf-8'
    to_encoding='gbk'
    for f in $file_list
    do
        iconv $f -f utf-8 &> /dev/null
        if [ $? ]; then
            echo "Convert the encoding of file $f to $to_encoding"
            iconv $f -f $from_encoding -t $to_encoding > .iconv.tmp
            mv .iconv.tmp $f
        else
            echo "The encoding of file $f is not $from_encoding , skip."
            continue
        fi
    done