https://juejin.cn/post/6935365727205457928

前言

在《学习如何编写 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 则退出交互模式。

思路分析
  1. 打印字符串很简单,直接 echo 打印即可。
  2. 删除字符串也不难,使用 ${变量名//旧字符串/新字符串} ,把 Hadoop 替换为空就从原字符串中删除了。
  3. 至于替换也是使用 ${变量名/旧字符串//新字符串}
  4. 用户输入,则使用的是上一篇文章讲到的 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
do
echo "$string" # 打印字符串
echo # 打印一个空
print_tips # 输出提示信息
read -p "请输入你的选择(1|2|3|4|q|Q):" choice # 接收用户输入
# 根据用户的输入,进入不同的分支,调用不同的函数
case $choice in
1)
len_of_string
;;
2)
delete_Hadoop
;;
3)
rep_hadoop_mapreduce_first
;;
4)
rep_hadoop_mapreduce_all
;;
q|Q)
exit
;;
*)
echo "错误的输入"
;;
esac
done
复制代码

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

运算

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

  • $((...))
  • expr
  • let
  • bc
  • declare -i

$((...))

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

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

expr

语法格式: 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 # 无限循环接收用户输入
do
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++))
do
sum=`expr $sum + $i` # 获取运算结果总和
done
echo "1+2+3+....+$num = $sum" # 输出运算结果
exit # 退出循环
fi
fi
echo "error,input enlegal" # 表示输入的值有误
continue # 如果输入有误继续循环让用户输入
done 复制代码

let 命令

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

let "foo = 1 + 2"
echo $foo # 3
复制代码

bc

支持浮点数运算。

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

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

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

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

declare -i val1=12 val2=5
declare -i result
result=val1*val2
echo $result # 60
复制代码
-x

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

declare -x foo=3 

# 等同于
export foo=3
复制代码
-r

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

declare -r bar=1

# 如果此时更改bar
bar=2 # -bash: bar:只读变量
复制代码
-u

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

declare -u foo
foo=upper
echo $foo # UPPER
复制代码
-l

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

declare -l bar
bar=LOWER
echo $bar # lower
复制代码
-p

-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[@]}
do
done
复制代码

文本三剑客

在之前的文章中也学习过 grep 命令了,这里为了统一学习文本三剑客,因此再复习下 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 /etc , Linux 中还有一个 rgrep 命令,作用相当于 grep -r

高级用法

grep 可以配合正则表达式使用。

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

sed

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 是一个文本处理工具,通常用于处理数据并生成结果报告。

语法格式

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 非常类似,都是按行进行匹配。

模式匹配的两种用法:

  1. 按正则表达式匹配。
  2. 运算符匹配。

正则匹配:

# 匹配/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}
else
{动作3}
复制代码

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

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

awk 中的循环语句

语法格式:

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

while 循环:

创建文件:while.awk
BEGIN{
while(i<=100){
sum+=i
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{
do
{
sum+=i
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
# I
# have
# a
复制代码

通过 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
Copyright (C) 1989, 1991-2012 Free Software Foundation.
...
复制代码

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
} END{
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 相关文章链接:

[转帖]学习如何编写 Shell 脚本(进阶篇)的更多相关文章

  1. Linux运维之shell脚本进阶篇

    一.if语句的使用 1)语法规则 if [条件] then 指令 fi 或 if [条件];then 指令 fi 提示:分号相当于命令换行,上面两种语法等同特殊写法:if[ -f"$file ...

  2. [转帖]编写shell脚本所需的语法和示例

    编写shell脚本所需的语法和示例 https://blog.csdn.net/CSDN___LYY/article/details/100584638 在说什么是shell脚本之前,先说说什么是sh ...

  3. shell脚本进阶之条件测试与条件语句

       接着上篇博客,今天整理一下关于条件测试和条件语句方面的知识. shell脚本进阶之条件测试    在编写shell脚本时,经常需要对一些条件进行判断,可以使用测试命令test辅助完成测试过程.t ...

  4. linux 的基本操作(编写shell 脚本)

    终于到shell 脚本这章了,在以前笔者卖了好多关子说shell脚本怎么怎么重要,确实shell脚本在linux系统管理员的运维工作中非常非常重要.下面笔者就带你正式进入shell脚本的世界吧. 到现 ...

  5. Linux学习-->如何通过Shell脚本实现发送邮件通知功能?

    1.安装和配置sendmail 不需要注册公网域名和MX记录(不需要架设公网邮件服务器),通过Linux系统自带的mail命令即可对公网邮箱发送邮件.不过mail命令是依赖sendmail的,所以我们 ...

  6. Git学习-->如何通过Shell脚本自动定时将Gitlab备份文件复制到远程服务器?

    一.背景 在我之前的博客 git学习--> Gitlab如何进行备份恢复与迁移? (地址:http://blog.csdn.net/ouyang_peng/article/details/770 ...

  7. 如何使用zx编写shell脚本

    前言 在这篇文章中,我们将学习谷歌的zx库提供了什么,以及我们如何使用它来用Node.js编写shell脚本.然后,我们将学习如何通过构建一个命令行工具来使用zx的功能,帮助我们为新的Node.js项 ...

  8. 编写shell脚本遇到的问题

    运行shell脚本提示“syntax error near unexpected token for((i=0;i<$length;i++))”: 原因是因为Linux下的换行符是 \n 而你在 ...

  9. 在windows下编写shell脚本

    注意两点: 1.第一行:#!/bin/bash 2.将文档格式转换为unix,因为在windows下编写shell脚本回车符是\n\r,而linux下的回车符是\n,所以在linux下运行脚本的时候, ...

  10. 编写Shell脚本的最佳实践

    编写Shell脚本的最佳实践 http://kb.cnblogs.com/page/574767/ 需要记住的 代码有注释 #!/bin/bash # Written by steven # Name ...

随机推荐

  1. BUUCTF Web CyberPunk WriteUp

    想直接查看payload的点这里 前言 二次注入(Second-Order Injection)是指攻击者在应用程序中注入恶意数据,然后在稍后的操作或不同的上下文中再次使用该恶意数据,导致安全漏洞.它 ...

  2. Luogu1419 区间问题 二分 单调优化

    原题链接 题意 给定一段长度为1e5的序列A,并且给我们一个范围 \([S, T]\), 要求我们求出一段长度在这个范围内的连续子序列,并且要使这个连续子序列的平均值最大,输出这个平均值. 思路 一开 ...

  3. 【华为云技术分享】40%性能提升,华为云推出PostgreSQL 12 商用版

    摘要:日前,华为云数据库正式推出了RDS for PostgreSQL 12版本,并开始商用.本文将从华为云RDS for PostgreSQL 12的4大特性和架构图等多方面来解读华为云Postgr ...

  4. 云小课|MRS基础原理之CarbonData入门

    阅识风云是华为云信息大咖,擅长将复杂信息多元化呈现,其出品的一张图(云图说).深入浅出的博文(云小课)或短视频(云视厅)总有一款能让您快速上手华为云.更多精彩内容请单击此处. 摘要:CarbonDat ...

  5. 如何给网页和代码做HTML加密?

    ​ 如何给网页和代码做HTML加密? 本篇文章给大家谈谈html混淆加密在线,以及HTML在线加密对应的知识点,希望对各位有所帮助,不要忘了收藏本站喔. ​ 如何给代码加密? 1.源代码加密软件推荐使 ...

  6. Solon2 开发之IoC,三、注入或手动获取 Bean

    1.如何注入Bean? 先了解一下Bean生命周期的简化版: 运行构建函数 尝试字段注入(有时同步注入,没时订阅注入.不会有相互依赖而卡住的问题) @Init 函数(是在容器初始化完成后才执行) .. ...

  7. 【开源项目推荐】——纯中文本地GPT知识库搭建项目.assets

    大家好,我是独孤风. 又到了本周的开源项目推荐.近一年多的时间,人工智能迎来了大爆发.GPT相关的大模型的发展让很多领域都发生了巨大的变化. 但是虽然GPT的自然语言识别功能异常的强大,但回答给我们的 ...

  8. xv6book阅读 chapter2

    一个操作系统至少应该满足三个需求:多路复用.隔离.交互.本章主要介绍如何组织操作系统来实现以上的三个需求,本文关注的是一种围绕单核进行设计的方法,这种设计是被许多uinx操作系统所使用的.Xv6运行在 ...

  9. 最被低估的Python绘图库!Matlplotlib 超强实力鉴赏

    最被低估的Python绘图库!Matlplotlib 超强实力鉴赏 Matplotlib Matplotlib 是一个 Python 的 2D绘图库,它以各种硬拷贝格式和跨平台的交互式环境生成出版质量 ...

  10. C# async await 异步执行方法封装 替代 BackgroundWorker

    BackWork代码: using System; using System.Collections.Generic; using System.Linq; using System.Text; us ...