相关文章推荐
苦闷的烤红薯  ·  Java开发工具IntelliJ ...·  5 月前    · 
贪玩的羊肉串  ·  使用git svn ...·  7 月前    · 
憨厚的绿豆  ·  leaflet ...·  1 年前    · 

在《 学习如何编写 Shell 脚本(基础篇) 》一文中已经讲解了 Shell 脚本编程的基础知识,如果还有不熟悉的同学可以去回顾下,本文将主要讲解 Shell 脚本的进阶知识,以及进阶必备秘籍文本处理三剑客。

如果本文对你有所帮助,请点个👍 👍 👍 吧。

字符串替换

替换规则:

  • ${变量名#匹配规则} 从变量开头进行规则匹配,将符合最短的数据删除。
  • ${变量名##匹配规则} 从变量开头进行规则匹配,将符合最长的数据删除。
  • ${变量名%匹配规则} 从变量尾部进行规则匹配,将符合最短的数据删除。
  • ${变量名%%匹配规则} 从变量尾部进行规则匹配,将符合最长的数据删除。
  • ${变量名/旧字符串/新字符串} 变量内容符合旧字符串,则第一个旧字符串会被新字符串取代。
  • ${变量名//旧字符串/新字符串} 变量内容符合旧字符串,则全部旧字符串会被新字符串取代。
  • var_1="I love you, Do you love me"
    var=${var_1#*ov} # e you, Do you love me
    var=${var_1##*ov} # e me
    var=${var_1%ov*} # I love you, Do you l
    var=${var_1%%ov*} # I l
    var_2="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
    # 第一个小写bin被替换为大写的BIN
    var=${var_2/bin/BIN} # /usr/local/BIN:/usr/bin:/bin:/usr/sbin:/sbin 
    # 所有小写bin会被替换为大写的BIN
    var=${var_2//bin/BIN} # /usr/local/BIN:/usr/BIN:/BIN:/usr/sBIN:/sBIN
    

    计算字符串的长度

  • ${#string} 
  • expr length "$string" 如果 string 有空格则必须加双引号。
  • var_1="Hello world"
    len=${#var_1} # 11
    len=`expr length "$var_1"` # 11
    

    获取子串在字符串中的索引位置

  • expr index $string $substring 从1开始计算索引位置。
  • var_1="quickstart is an app"
    ind=`expr index "$var_1" start` # 6
    ind=`expr index "$var_1" uniq` # 1
    ind=`expr index "$var_1" f` # 0
    

    它其实是按照子串的每个字符每个字符去进行匹配

  • 第一个例子匹配到的是 s 位置6。
  • 第二个匹配到的是 q 位置1。
  • 第三个例子什么都没有匹配到所以位置是0。
  • 计算子串长度

    expr match $string substr从头开始匹配子串长度,如果没有匹配到则返回0,匹配到了则返回配的子串长度。

    var_1="quickstart is an app"
    sub_len=`expr match "$var_1" app` # 0
    sub_len=`expr match "$var_1" quic` # 4
    sub_len=`expr match "$var_1" quic.*` # 18
    
  • ${string:position} 从 string 的 position 开始。
  • ${string:position:length} 从 position 开始,匹配长度为 length 。
  • ${string: -position} 从右边开始匹配。
  • ${string:(position)} 从左边开始匹配。
  • expr substr $string $postion $length 从 position 开始,匹配长度为 length 。
  • var_1="quickstartisanapp"
    # 从索引10到最后提取子串,这种方式的索引是从0开始计算的
    substr=${var_1:10} # isanapp
    substr=${var_1:10:2} # is
    # -5 前面需要添加空格,如果不添加空格也可以使用括号
    substr=${var_1: -5} # anapp
    substr=${var_1:(-5)} # anapp
    # expr 这种方式的索引是从1开始
    substr=`expr substr "$var_1" 10 5` # tisan
    

    字符串实战练习

    变量 string="Bigdata process framework is Hadoop,Hadoop is an open source project"  执行脚本后,打印输出 string 字符串变量,并给用户以下选项:

  • (1)、打印 string 长度
  • (2)、删除字符串中所有的 Hadoop 
  • (3)、替换第一个 Hadoop 为 Mapreduce 
  • (4)、替换全部 Hadoop 为 Mapreduce 
  • 用户输入数字 1|2|3|4 ,可以执行对应项的功能;输入 q|Q 则退出交互模式。

  • 打印字符串很简单,直接 echo 打印即可。
  • 删除字符串也不难,使用 ${变量名//旧字符串/新字符串} ,把 Hadoop 替换为空就从原字符串中删除了。
  • 至于替换也是使用 ${变量名/旧字符串//新字符串}
  • 用户输入,则使用的是上一篇文章讲到的 read 命令。
  • 好了这个练习的思路非常简单,下面就让我们直接编写代码吧。

    #!/bin/bash
    string="Bigdata process framework is Hadoop,Hadoop is an open source project"
    # 打印提示函数
    print_tips(){
      echo "(1)、打印string长度"
      echo "(2)、删除字符串中所有的Hadoop"
      echo "(3)、替换第一个Hadoop为Mapreduce"
      echo "(4)、替换全部Hadoop为Mapreduce"
    # 输出字符串长度函数
    len_of_string(){
      echo "${#string}"
    # 替换Hadoop子串为空,相当于删除
    delete_Hadoop(){
      echo "${string//Hadoop/}"
    # 替换一个Hadoop为Mapreduce
    rep_hadoop_mapreduce_first(){
      echo "${string/Hadoop/Mapreduce}"
    # 替换全部Hadoop为Mapreduce
    rep_hadoop_mapreduce_all(){
      echo "${string//Hadoop/Mapreduce}"
    # 无限循环的接收用户输入,直到用户输入了q|Q则退出进程
    while true
      echo "$string" # 打印字符串
      echo # 打印一个空
      print_tips # 输出提示信息
      read -p "请输入你的选择(1|2|3|4|q|Q):" choice # 接收用户输入
      # 根据用户的输入,进入不同的分支,调用不同的函数
      case $choice in
        	len_of_string
        	delete_Hadoop
        	rep_hadoop_mapreduce_first
        	rep_hadoop_mapreduce_all
        	echo "错误的输入"
    

    这样我们就轻轻松松完成了一个 shell 脚本了。

    bash 中的运算方式有以下几种:

  • $((...)) 
  • declare -i 
  • $((...))

    echo $((2 + 2)) # 加法
    echo $((5 / 2)) # 除法
    echo $((i++)) # 先返回值后运算
    echo $((++i)) # 先运算后返回值
    echo $(( (2 + 3) * 4 )) # 圆括号改变运算顺序
    echo $((0xff)) # 16进制转成10进制的运算
    echo $((16>>2)) # 位运算
    echo $((3 > 2)) # 逻辑运算,如果逻辑表达式为真,返回1,否则返回0
    echo $((a<1 ? 1 : 0)) # 三元运算符
    echo $((a=1)) # 变量赋值
    

    [注意] 这个语法只能计算整数。

    语法格式: expr $num1 operator $num2

    操作符概览:

  • num1 | num2 -- num1 不为空且非0,返回 num1 ; 否则返回 num2  
  • num1 & num2 -- num1 不为空且非0,返回 num1 ;否则返回0
  • num1 < num2 -- num1 小于 num2 ,返回1;否则返回0
  • num1 <= num2 -- num1 小于等于 num2 ,返回1;否则返回0
  • num1 = num2 -- num1 等于 num2 ,返回1;否则返回0
  • num1 != num2 -- num1 不等于 num2 ,返回1;否则返回0
  • num1 > num2  -- num1 大于 num2 ,返回1;否则返回0
  • num1 >= num2  -- num1  大于等于 num2 ,返回1;否则返回0
  • num1 + num2 -- 求和
  • num1 - num2 -- 求差
  • num1 * num2 -- 求积
  • num1 / num2 -- 求商
  • num1 % num2 -- 求余
  • $num1=30; $num2=50
    expr $num1 \> $num2 # 大于不成立,返回0
    expr $num1 \< $num2 # 小于成立,返回1
    expr $num1 \| $num2 # 返回 num1
    expr $num1 \& $num2 # 返回 num1
    expr $num1 + $num2 # 计算加法
    num3=`expr $num1 + $num2` # 计算结果赋值给num3
    expr $num1 - $num2 # 计算减法
    

    [注意] >< 等操作符是 Shell 中的保留关键字,因此需要进行转义。否则就变成输出和输入重定向了。

    提示用户输入一个正整数 num ,然后计算从 1+2+3 加到给定正整数。 必须对给定的 num 值进行是否为正整数判断,如果不是正整数则重新输入。

    代码实现:

    #!/bin/bash
    while true # 无限循环接收用户输入
    	read -p "pls input a positive number: " num # 接收到用户的输入,存为num值
    	expr $num + 1 2>&1 /dev/null # 对num进行加1的运算,运算结果重定向到/dev/null
          # 由于 expr 只能运算整数,如果运算浮点数的话会报错,$?获取的是表达式执行结果,并非运算结果
          # 执行结果如果是正常的返回0
    	if [ $? -eq 0 ];then # $? 获取到上一次运算结果
    		if [ `expr $num \> 0` -eq 1 ];then # 上面判断了是否为整数,这里判断是否为正整数
        	# 类似JavaScript循环的写法遍历一个数值
    			for((i=1;i<=$num;i++))
    				sum=`expr $sum + $i` # 获取运算结果总和
    			echo "1+2+3+....+$num = $sum" # 输出运算结果
    			exit # 退出循环
    	echo "error,input enlegal" # 表示输入的值有误
    	continue # 如果输入有误继续循环让用户输入
    

    let 命令

    let 命令声明变量时,可以直接执行算术表达式。

    let "foo = 1 + 2"
    echo $foo # 3
    

    支持浮点数运算。

    echo "scale=2;23/5" | bc # scale表示浮点位数
    num1=`echo "scale=2;23/5" | bc` # 运算结果保存为变量
    

    declare -i 命令

    -i 参数声明整数变量以后,可以直接进行数学运算。

    declare -i val1=12 val2=5
    declare -i result
    result=val1*val2
    echo $result # 60
    

    获取某一段命令的执行结果,它的方式有两种:

  • `command`
  • $(command) 
  • all_files=`ls` # 获取ls命令的执行结果
    all_files=$(ls) # 效果同上
    

    这两种命令替换的方式都是等价的,可以任选其一使用。

    获取系统的所有用户并输出

    #!/bin/bash
    index=1
    for user in `cat /etc/passwd | cut -d ":" -f 1`
    	# cat /etc/passwd 获取系统中的所有用户和密码
    	# cut -d ":" -f 1 根据冒号切每一行的字符串,获取切好后的第一部分
    	# 使用for循环进行遍历,并输出用户
    	echo "This is $index user: $user"
    	index=$(($index + 1))
    

    获取今年已经过了多少天和周

    echo "This year have passed $(date +%j) days"
    echo "This year have passed $(($(date +%j)/7)) weeks"
    

    [注意] $(()) 用于数学运算, $() 用于命令替换,这两个是平时使用中特别容易混淆的语法。

    declare

    declare 命令可以声明一些特殊类型的变量,为变量设置一些限制,比如声明只读类型的变量和整数类型的变量。

    declare OPTION VARIABLE=value
    
  • -r 将变量设为只读;
  • -i 将变量设为整数;
  • -a 将变量定义为数组;
  • -f 显示此脚本前定义过的所有函数及内容;
  • -F 仅显示此脚本前定义过的函数名;
  • -x 将变量声明为环境变量。
  • declare命令如果用在函数中,声明的变量只在函数内部有效,等同于local命令。

    不带任何参数时,declare命令输出当前环境的所有变量,包括函数在内,等同于不带有任何参数的set命令。

    -i 参数声明整数变量以后,可以直接进行数学运算。

    declare -i val1=12 val2=5
    declare -i result
    result=val1*val2
    echo $result # 60
    

    -x 参数等同于 export 命令,可以输出一个变量为子 Shell 的环境变量。

    declare -x foo=3 
    # 等同于
    export foo=3 
    

    -r 参数可以声明只读变量,无法改变变量值,也不能 unset 变量。

    declare -r bar=1
    # 如果此时更改bar
    bar=2 # -bash: bar:只读变量
    

    -u 参数声明变量为大写字母,可以自动把变量值转成大写字母。

    declare -u foo
    foo=upper
    echo $foo # UPPER
    

    -l 参数声明变量为小写字母,可以自动把变量值转成小写字母。

    declare -l bar
    bar=LOWER
    echo $bar # lower
    

    -p 参数输出变量信息。

    foo=hello
    declare -p foo #控制台输出:declare -x foo="hello"
    
    array=('v1' 'v2' 'v3') 
    

    输出数组内容

    ${array[@]} # 输出全部内容
    ${array[*]} # 也是输出全部内容
    ${array[1]} # 输出下标索引为1的内容
    

    获取数组长度

    ${#array} # 数组内元素个数
    ${#array[2]} # 数组内下标为2的元素长度
    
    array[0]="frank" # 给数组下标为1的元素赋值为frank
    array[20]="lion" # 在数组尾部添加一个新元素
    
    unset array[2] # 清除元素
    unset array # 清空整个数组
    
    for v in ${array[@]}
    

    文本三剑客

    在之前的文章中也学习过 grep 命令了,这里为了统一学习文本三剑客,因此再复习下 grep 命令。

    全局搜索一个正则表达式,并且打印到屏幕。简单来说就是,在文件中查找关键字,并显示关键字所在行。

    grep text file # text代表要搜索的文本,file代表供搜索的文件
    [root@lion ~]# grep path /etc/profile
    pathmunge () {
        pathmunge /usr/sbin
        pathmunge /usr/local/sbin
        pathmunge /usr/local/sbin after
        pathmunge /usr/sbin after
    unset -f pathmunge
    
  • -i 忽略大小写, grep -i path /etc/profile
  • -n 显示行号,grep -n path /etc/profile
  • -v 只显示搜索文本不在的那些行,grep -v path /etc/profile
  • -r 递归查找, grep -r hello /etcLinux 中还有一个 rgrep 命令,作用相当于 grep -r
  • grep 可以配合正则表达式使用。

    grep -E path /etc/profile --> 完全匹配path
    grep -E ^path /etc/profile --> 匹配path开头的字符串
    grep -E [Pp]ath /etc/profile --> 匹配path或Path
    

    stream Editor 的缩写,流编辑器,对标准输出或文件逐行进行处理。

    sed [option] "pattern/command" file
    sed '/python/p' name.txt # 省略了option,/python/为pattern正则,p为command命令打印的意思
    

    [注意] 匹配模式中存在变量要使用双引号。

    选项 option

    -n 只打印模式匹配行
    sed 'p' name.txt # 会对没一行字符串输出两遍,第一遍是原文本,第二遍是模式匹配到的文本
    sed -n 'p' name.txt # 加了-n参数后就只打印模式匹配到的行
    
    -e 默认选项

    支持多个 pattern command 的形式

    sed -e "pattern command" -e "pattern command" file
    
    -f 指定动作文件
    # 简单命令
    sed -n '/python/p' name.txt # 但是一旦正则比较复杂,我们就可以把它保存在一个单独文件中,使用-f进行指定
    # edit.sed 内容为 /python/p
    sed -n -f edit.sed name.txt # '/python/p'这个命令保存在edit.sed文件中,使用-f指定
    
    -E 扩展表达式
    sed -n -E '/python|PYTHON/p' name.txt
    
    -i 直接修改文件内容
    sed -i 's/love/like/g' name.txt # -i修改原文件,全局love替换为like
    

    模式匹配 pattern

    pattern 用法表

    匹配模式含义用法
    10command 匹配到第10行sed -n "17p" name.txt  打印 name 文件的第17行
    10,20command 匹配从第10行开始,到第20行结束sed -n "10,20p" name.txt 打印 name 文件的10到20行
    10,+5command 匹配从第10行开始,到第15行结束sed -n "10,+5p" name.txt 
    /pattern1/command 匹配到 pattern1 的行sed -n "/^root/p" name.txt 打印 root 开头的行
    /pattern1/,/pattern2/command 匹配到 pattern1 的行开始,至匹配到 pattern2 的行结束sed -n "/^ftp/,/^mail/p" name.txt 
    10,/pattern1/command 匹配从第10行开始,到匹配到 pattern1 的行结束sed -n "4,/^ok/p" name.txt 打印 name 文件从第4行开始匹配,直到以 ok 开头的行结束
    /pattern1/,10command 匹配到 pattern1 的行开始,到第10行匹配结束sed -n "/root/,10p" name.txt 

    命令 command

  • p -- 打印
  • a -- 行后追加
  • i -- 行前追加
  • r -- 外部文件读入,行后追加
  • w -- 匹配行写入外部文件
  • d -- 删除
  • s/old/new -- 将行内第一个 old 替换为 new
  • s/old/new/g -- 将行内全部的 old 替换为 new
  • s/old/new/2g --同一行,只替换从第二个开始到剩下所有的
  • s/old/new/ig -- 将行内 old 全部替换为 new ,忽略大小写
  • sed '/python/p' name.txt # 打印
    sed '1d' name.txt # 删除第一行
    sed -i '/frank/a hello' name.txt # 匹配到字符串frank就在这一行后面插入“hello”
    sed -i '/frank/r list' name.txt # 把list文件内容追加到匹配frank字符串后面的行
    sed -n '/frank/w tmp.txt' name.txt # 将匹配到frank的行全部写入tmp.txt文件中
    sed -i 's/love/like/g' name.txt # 全局替换love为like,并修改源文件
    

    引用前面的匹配到的字符。

    假设有一个文件 test.txt 内容为:

    a heAAo vbv
    c heBBo sdsad
    k heCCo mnh
    

    现在需要匹配到 heAAo heBBo heCCo 并在他们后面添加 s 。

    使用反向引用可以这样做:

    sed -i 's/he..o/&s/g' test.txt # 其中&就是引用前面匹配到的字符
    sed -i 's/\(he..o\)/\1s/g' test.txt # 与上面的写法等价
    # 输出结果
    a heAAos vbv
    c heBBos sdsad
    k heCCos mnh
    

    awk 是一个文本处理工具,通常用于处理数据并生成结果报告。

    awk 'BEGIN{}pattern{commands}END{}' file_name
    
  • BEGIN{} 正式处理数据之前执行;
  • pattern 匹配模式;
  • {commands} 处理命令,可能多行;
  • END{} 处理完所有匹配数据后执行。
  • $0 -- 整行内容。
  • $1-$n -- 当前行的第 1-n 个字段。
  • NF -- Number Field 的缩写,表示当前行的字段个数,也就是有多少列。
  • NR -- Number Row 的缩写,表示当前行的行号,从1开始计数。
  • FNR -- File Number Row 的缩写,表示多文本处理时,每个文件行号单独计数,都是从1开始。
  • FS -- Field Separator 的缩写,表示输入字段分隔符,不指定默认以空格或 tab 键分割。
  • RS -- Row Separator 的缩写,表示输入行分隔符,默认回车换行 \n 。
  • OFS -- Output Field Separator ,表示输出字段分隔符,默认为空格。
  • ORS -- Output Row Separator 的缩写,表示输出行分隔符,默认为回车换行。
  • FILENAME -- 当前输入的文件名字。
  • ARGC -- 命令行参数个数。
  • ARGV -- 命令行参数数组。
  • [注意] 字段个数,非字符个数,例如 "frank lion alan" 这一行有3个字段。

    awk '{print $0}' /etc/passwd # 只有处理命令,输出每一行的内容
    awk 'BEGIN{FS=":"}{print $1}' /etc/passwd # 处理文本之前先定义好分隔符为冒号,然后打印每一行被冒号分割后的文本的第一个字段
    awk 'BEGIN{FS=":"}{print $NF}' /etc/passwd # 输出没一行的最后一列,NF中存的总列数,因此$NF表示最后一列的字段
    awk '{print $NF}' /etc/passwd # 输出每一行的字段个数(默认以空格分割后的字段个数)
    awk '{print NR}' /etc/passwd # 输出文件的行号
    awk '{print FNR}' /etc/passwd name.txt # 输出两个文件的行号,每个文件单独计数
    awk 'BEGIN{RS="---"}{print $0}' /etc/passwd # 行分隔符设置为---
    awk 'BEGIN{ORS="---"}{print $0}' /etc/passwd # 定义输出的每一行的分隔符为 --- 
    awk 'BEGIN{ORS="---";RS="---"}{print $0}' /etc/passwd # BEGIN 中定义多个变量需要;分割
    awk '{print FILENAME}' name.txt # 输出name.txt
    

    [备注] /etc/passwd 就是 Linux 系统中的密码文本,后续会一直使用,大概是如下格式:

    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
    

    printf

    awk 格式化输出。

  • %s -- 打印字符串;
  • %d -- 打印10进制数;
  • %f -- 打印浮点数;
  • %x -- 打印16进制数;
  • %o -- 打印8进制数;
  • %e -- 打印数字的科学计数法格式;
  • %c -- 打印单个字符的 ASCII 码。
  • - -- 左对齐;
  • + -- 右对齐;
  • # -- 显示8进制在前面加0,显示16进制在前面加 0x 。
  • awk 'BEGIN{FS=":"}{printf $1}' /etc/passwd
    

    输入上面 awk 命令打印后得到这样的结果: 不再像使用 print 命令会默认以换行符作为默认的输出了,而是全部集中在一行,因此使用 printf 需要自定义输出格式:

    awk 'BEGIN{FS=":"}{printf "%s\n",$1}' /etc/passwd # %s表示打印字符串 \n表示换行
    

    是不是认为直接使用 print 也可以简单的做到,何必使用 printf 呢,但其实 printf 在处理多个字段的分隔符上是非常方便的。

    awk 'BEGIN{FS=":"}{printf "%20s %20s\n",$1,$7}' /etc/passwd
    
  • %20s 表示 20 个字符,如果实际字符不够会默认使用空格补全
  • $1 表示第一部分, $7 表示第 7 部分
  • 默认是右对齐,我们也可以添加修饰符,使其左对齐:

    awk 'BEGIN{FS=":"}{printf "%-20s %-20s\n",$1,$7}' /etc/passwd
    

    其它示例:

    awk 'BEGIN{FS=":"} {printf "%d\n", $3}' /etc/passwd # 打印十进制数字,默认是左对齐的
    awk 'BEGIN{FS=":"} {printf "%20d\n", $3}' /etc/passwd # %20d 不够的位数默认补全空格,因此它是右对齐
    awk 'BEGIN{FS=":"} {printf "%f\n", $3}' /etc/passwd # 打印浮点数,默认保留小数点后6位置
    awk 'BEGIN{FS=":"} {printf "%0.2f\n", $3}' /etc/passwd # 打印浮点数,保留小数点后2位置
    

    模式匹配 pattern

    它与 sed 的 pattern 非常类似,都是按行进行匹配。

    模式匹配的两种用法:

  • 按正则表达式匹配。
  • 运算符匹配。
  • 正则匹配:

    # 匹配/etc/passwd文件行中包含root字符串的所有行
    awk '/root/{print $0}' /etc/passwd
    # 匹配/etc/passwd文件行中以lion开头的所有行
    awk '/^lion/{print $0}' /etc/passwd
    

    运算符匹配:

  • <= 小于等于
  • >= 大于等于
  • == 等于
  • != 不等于
  • ~ 匹配正则表达式
  • !~ 不匹配正则表达式
  • || 或
  • # 以:为分隔符,匹配/etc/passwd文件中第三个字段小于50的所有信息
    awk 'BEGIN{FS=":"}$3<50{print $0}' /etc/passwd
    # 输出第三个字段值等于1的行
    awk 'BEGIN{FS=":"}$3==1{print $0}' /etc/passwd
    # 输出第七个字段值等于 /bin/bash 的行
    awk 'BEGIN{FS=":"}$7=="/bin/bash"{print $0}' /etc/passwd
    # 输出第三个字段值的位数大于3的行
    awk 'BEGIN{FS=":"}$3~/[0-9]{3,}/{print $0}' /etc/passwd
    # 以:为分隔符,匹配/etc/passwd文件中第一个字段包含hdfs或yarn的所有行信息
    awk 'BEGIN{FS=":"}$1=="lion" || $1=="frank" {print $0}' /etc/passwd
    # 以:为分隔符,匹配/etc/passwd文件中第三个字段小于50并且第四个字段大于50的所有行信息
    awk 'BEGIN{FS=":"}$3<50 && $4>50{print $0}' /etc/passwd
    

    awk 中的表达式

    awk 'BEGIN{var=20;var1="aaa";print var,var1}' # 输出 20 aaa
    awk 'BEGIN{n1=20;n2+=n1;print n1,n2}' # 20 20
    awk 'BEGIN{n1=20;n2=30;printf "%0.2f\n",n1/n2}' # 输出浮点数,精确到小数点后两位
    

    [注意] 在 BEGIN 和 END 中都可以正常执行表达式。

    巩固习题:使用 awk 计算 /etc/services 中的空白行数量
    # BEGIN 定义变量,通过正则/^$/匹配到空行后,执行sum++,全面匹配结束后在打印sum的值
    awk 'BEGIN{sum=0}/^$/{sum++}END{print sum}' /etc/services 
    

    awk 中的条件语句

    语法格式:

    if(条件表达式)
    	{动作1} 
    else if(条件表达式)
    	{动作2}
    	{动作3}
    

    练习:以冒号为分隔符,只打印 /etc/passwd 中第3个字段的数值在 50-100 范围内的行信息

    awk 'BEGIN{FS=":"}{ if($3>50 && $3<100){print $0} }' /etc/passwd
    

    awk 中的循环语句

    语法格式:

    # while 循环
    while(条件表达式)
    # do while 循环
    while(条件表达式)
    # for循环
    for(初始化计数器;计数器测试;计数器变更)
    
    计算 1+2+3+...+100 的和

    while 循环:

    创建文件:while.awk
    BEGIN{
      while(i<=100){
        sum+=i
      print sum
    执行命令:awk -f while.awk # 以脚本的方式执行awk动作
    

    for 循环:

    创建文件:for.awk
    BEGIN{
      for(i=0;i<=100;i++)
      	sum+=i
      print sum
    执行命令:awk -f for.awk # 以脚本的方式执行awk动作
    

    do...while 循环:

    创建文件:do-while.awk
    BEGIN{
        sum+=i
      }while(i<=100)
      print sum
    执行命令:awk -f do-while.awk # 以脚本的方式执行awk动作
    

    awk 字符串函数

  • length(str) 计算长度;
  • index(str1,str2) 返回在 str1 中查询到的 str2 的位置;
  • tolower(str) 小写转换;
  • toupper(str) 大写转换;
  • split(str,arr,fs) 分隔符字符串,并保存到数组中;
  • match(str,RE) 返回正则表达式匹配到的子串位置;
  • substr(str,m,n) 截取子串,从 m 个字符开始,截取 n 位。若 n 不指定,则默认截取到字符串末尾;
  • sub(RE,ReqStr,str) 替换查找到的第一个子串;
  • gsub(RE,ReqStr,str) 替换查找到的所有子串。
  • length(str)

    计算字符长度

    awk 'BEGIN{FS=":"}{print length($3)}' /etc/passwd # 计算第三个字段的长度
    
    index(str1,str2)

    返回在 str1 中查询到的 str2 的位置

    awk 'BEGIN{print index("I have a dream","ea")}' # 查找ea在字符串中的位置
    
    tolower(str)

    转换为小写字符

    awk 'BEGIN{print tolower("I have a dream")}' # i have a dream
    
    toupper(str)

    转换为大写字符

    awk 'BEGIN{print toupper("I have a dream")}' # I HAVE A DREAM
    
    split(str,arr,fs)

    分隔符字符串,并保存到数组中

    # 将字符串"I have a dream"按照空格进行分割
    awk 'BEGIN{str="I have a dream";split(str,arr," ");for(a in arr) print arr[a]}'
    # dream
    # have
    

    通过 for…in 得到是无序的数组。如果需要得到有序数组,需要通过下标获得:

    awk 'BEGIN{str="I have a dream";len = split(str,arr," ");for(i=1;i<=len;i++) print arr[i]}'
    
    match(str,RE)

    返回正则表达式匹配到的子串位置

    # 输出字符串第一个数字出现所在的位置
    awk 'BEGIN{str="I have 1 dream";print match(str,/[0-9]/)}' # 8
    
    substr(str,m,n)

    截取子串,从 m 个字符开始,截取 n 位。若 n 不指定,则默认截取到字符串末尾。

    # 截取字符串"I have a dream"的子串,从第四个位置开始,一直到最后
    awk 'BEGIN{str="I have a dream";print substr(str,4)}' # ave a dream
    
    sub(RE,ReqStr,str)

    替换查找到的第一个子串。

  • RE 为正则表达式
  • ReqStr 为要替换的字符串
  • str 为源字符串
  • # 替换字符串"I have 123 dream"中第一个数字为$符号
    awk 'BEGIN{str="I have 123 dream";sub(/[0-9]+/,"$",str);print str}' # I have $ dream
    
    gsub(RE,ReqStr,str)

    替换查找到的所有子串

  • RE 为正则表达式
  • ReqStr 为要替换的字符串
  • str 为源字符串
  • # 替换字符串"I have 123 dream 456"中第一个数字为$符号
    awk 'BEGIN{str="I have 123 dream 456";gsub(/[0-9]+/,"$",str);print str}' # I have $ dream $
    

    选项 option

  • -v 参数传递
  • -f 指定脚本文件
  • -F 指定分隔符
  • -V 查看 awk 的版本号
  • awk 的 BEGIN 中不能直接使用 shell 中定义的变量,需要通过 -v 参数进行传递

    [root@lion ~]# var=20
    [root@lion ~]# num=30
    [root@lion ~]# awk -v var1="$var" -v var2="$num" 'BEGIN{print var1,var2}'
    20 30
    
    指定脚本文件

    'BEGIN{}pattern{commands}END{}' 这里的内容可以写在一个独立的文件中,通过 -f 参数来引入。

    # test.awk
    BEGIN{
      var=30
      print var
    awk -f test.awk # 输出30
    

    这种写法更易于编写和维护,适合复杂 awk 语句。

    指定分隔符
    awk 'BEGIN{FS=":"}{print length($3)}' /etc/passwd
    awk -F : '{print length($3)}' /etc/passwd
    
    查看 awk 的版本号
    [root@lion ~]# awk -V
    GNU Awk 4.0.2
    

    awk 处理生产数据实例

    假设我们有一堆以下格式数据保存在文件 data.txt 中:

    2020-01-01 00:01:01 1000 Batches: user frank insert 2010 records databases:product table:detail, insert 1000 records successfully,failed 1010 records
    2020-01-01 00:01:02 1001 Batches: user lion insert 1030 records databases:product table:detail, insert 989 records successfully,failed 41 records
    2020-01-01 00:01:03 1002 Batches: user mike insert 1290 records databases:product table:detail, insert 235 records successfully,failed 1055 records
    2020-01-01 00:01:04 1003 Batches: user alan insert 3452 records databases:product table:detail, insert 1257 records successfully,failed 2195 records
    2020-01-01 00:01:05 1004 Batches: user ben insert 4353 records databases:product table:detail, insert 2245 records successfully,failed 2108 records
    2020-01-01 00:01:06 1005 Batches: user bily insert 5633 records databases:product table:detail, insert 3456 records successfully,failed 2177 records
    2020-01-01 00:01:07 1006 Batches: user frank insert 2010 records databases:product table:detail, insert 1000 records successfully,failed 1010 records
    

    统计每个人员分别插入了多少条 record 进数据库,预期结果:

    USER	Total Records
    frank	4020
    lion  1030
    

    代码演示:

    # data.awk
    BEGIN{
    	printf "%-10s %-10s\n", "User", "Total Records"
    	USER[$6]+=$8 # USER为数组,空格分割后$6这个变量是用户名,$8是records,每次都去数组中取出$6的值进行相加以获取重复用户的总Records
        for(u in USER) # 遍历USER数组,打印数组每一项
        	printf "%-10s %-10d\n", u , USER[u]
    # awk 语句
    awk -f data.awk data.txt
    # 输出结果
    User       Total Records
    frank      4020      
    mike       1290      
    bily       5633      
    alan       3452      
    lion       1030      
    ben        4353 
    

    通过本文,我们学习了编写 Shell 脚本的一些进阶语法:操作字符串、算术运算、操作数组以及最核心的文本三剑客 grep 、 sed 和 awk 的详细用法。

    虽然讲解知识时贯穿有实际应用案例,但想彻底掌握这些知识,还需要多多实战。

    往期 Linux 相关文章链接:

  • 学习 Linux 命令(基础篇)
  • 学习 Linux 命令(进阶篇)
  • 十分钟掌握 Vim 编辑器核心功能
  • 学习如何编写 Shell 脚本(基础篇)
  • 都看到这里了,就点个👍 👍 👍 吧。