在《
学习如何编写 Shell 脚本(基础篇)
》一文中已经讲解了
Shell
脚本编程的基础知识,如果还有不熟悉的同学可以去回顾下,本文将主要讲解
Shell
脚本的进阶知识,以及进阶必备秘籍文本处理三剑客。
如果本文对你有所帮助,请点个👍 👍 👍 吧。
字符串替换
替换规则:
${变量名#匹配规则}
从变量开头进行规则匹配,将符合最短的数据删除。
${变量名##匹配规则}
从变量开头进行规则匹配,将符合最长的数据删除。
${变量名%匹配规则}
从变量尾部进行规则匹配,将符合最短的数据删除。
${变量名%%匹配规则}
从变量尾部进行规则匹配,将符合最长的数据删除。
${变量名/旧字符串/新字符串}
变量内容符合旧字符串,则第一个旧字符串会被新字符串取代。
${变量名//旧字符串/新字符串}
变量内容符合旧字符串,则全部旧字符串会被新字符串取代。
var_1="I love you, Do you love me"
var=${var_1#*ov}
var=${var_1##*ov}
var=${var_1%ov*}
var=${var_1%%ov*}
var_2="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
var=${var_2/bin/BIN}
var=${var_2//bin/BIN}
计算字符串的长度
${#string}
expr length "$string"
如果 string
有空格则必须加双引号。
var_1="Hello world"
len=${#var_1}
len=`expr length "$var_1"`
获取子串在字符串中的索引位置
expr index $string $substring
从1开始计算索引位置。
var_1="quickstart is an app"
ind=`expr index "$var_1" start`
ind=`expr index "$var_1" uniq`
ind=`expr index "$var_1" f`
它其实是按照子串的每个字符每个字符去进行匹配
第一个例子匹配到的是 s
位置6。
第二个匹配到的是 q
位置1。
第三个例子什么都没有匹配到所以位置是0。
计算子串长度
expr match $string substr
从头开始匹配子串长度,如果没有匹配到则返回0,匹配到了则返回配的子串长度。
var_1="quickstart is an app"
sub_len=`expr match "$var_1" app`
sub_len=`expr match "$var_1" quic`
sub_len=`expr match "$var_1" quic.*`
${string:position}
从 string
的 position
开始。
${string:position:length}
从 position
开始,匹配长度为 length
。
${string: -position}
从右边开始匹配。
${string:(position)}
从左边开始匹配。
expr substr $string $postion $length
从 position
开始,匹配长度为 length
。
var_1="quickstartisanapp"
substr=${var_1:10}
substr=${var_1:10:2}
substr=${var_1: -5}
substr=${var_1:(-5)}
substr=`expr substr "$var_1" 10 5`
字符串实战练习
变量 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}"
delete_Hadoop(){
echo "${string//Hadoop/}"
rep_hadoop_mapreduce_first(){
echo "${string/Hadoop/Mapreduce}"
rep_hadoop_mapreduce_all(){
echo "${string//Hadoop/Mapreduce}"
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))
echo $((16>>2))
echo $((3 > 2))
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
expr $num1 \< $num2
expr $num1 \| $num2
expr $num1 \& $num2
expr $num1 + $num2
num3=`expr $num1 + $num2`
expr $num1 - $num2
[注意] >
、<
等操作符是 Shell
中的保留关键字,因此需要进行转义。否则就变成输出和输入重定向了。
提示用户输入一个正整数 num
,然后计算从 1+2+3
加到给定正整数。
必须对给定的 num
值进行是否为正整数判断,如果不是正整数则重新输入。
代码实现:
#!/bin/bash
while true
read -p "pls input a positive number: " num
expr $num + 1 2>&1 /dev/null
if [ $? -eq 0 ];then
if [ `expr $num \> 0` -eq 1 ];then
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
支持浮点数运算。
echo "scale=2;23/5" | bc
num1=`echo "scale=2;23/5" | bc`
declare -i 命令
-i
参数声明整数变量以后,可以直接进行数学运算。
declare -i val1=12 val2=5
declare -i result
result=val1*val2
echo $result
获取某一段命令的执行结果,它的方式有两种:
`command`
$(command)
all_files=`ls`
all_files=$(ls)
这两种命令替换的方式都是等价的,可以任选其一使用。
获取系统的所有用户并输出
#!/bin/bash
index=1
for user in `cat /etc/passwd | cut -d ":" -f 1`
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
-x
参数等同于 export
命令,可以输出一个变量为子 Shell
的环境变量。
declare -x foo=3
export foo=3
-r
参数可以声明只读变量,无法改变变量值,也不能 unset
变量。
declare -r bar=1
bar=2
-u
参数声明变量为大写字母,可以自动把变量值转成大写字母。
declare -u foo
foo=upper
echo $foo
-l
参数声明变量为小写字母,可以自动把变量值转成小写字母。
declare -l bar
bar=LOWER
echo $bar
-p
参数输出变量信息。
foo=hello
declare -p foo
array=('v1' 'v2' 'v3')
输出数组内容
${array[@]}
${array[*]}
${array[1]}
获取数组长度
${#array}
${#array[2]}
array[0]="frank"
array[20]="lion"
unset array[2]
unset array
for v in ${array[@]}
文本三剑客
在之前的文章中也学习过 grep
命令了,这里为了统一学习文本三剑客,因此再复习下 grep
命令。
全局搜索一个正则表达式,并且打印到屏幕。简单来说就是,在文件中查找关键字,并显示关键字所在行。
grep text file
[root@lion ~]
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
stream Editor
的缩写,流编辑器,对标准输出或文件逐行进行处理。
sed [option] "pattern/command" file
sed '/python/p' name.txt
[注意] 匹配模式中存在变量要使用双引号。
选项 option
-n
只打印模式匹配行
sed 'p' name.txt
sed -n 'p' name.txt
-e 默认选项
支持多个 pattern command
的形式
sed -e "pattern command" -e "pattern command" file
-f 指定动作文件
sed -n '/python/p' name.txt
sed -n -f edit.sed name.txt
-E 扩展表达式
sed -n -E '/python|PYTHON/p' name.txt
-i 直接修改文件内容
sed -i 's/love/like/g' name.txt
模式匹配 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
sed -i '/frank/r list' name.txt
sed -n '/frank/w tmp.txt' name.txt
sed -i 's/love/like/g' name.txt
引用前面的匹配到的字符。
假设有一个文件 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
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
awk '{print FILENAME}' 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
是不是认为直接使用 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
awk 'BEGIN{FS=":"} {printf "%f\n", $3}' /etc/passwd
awk 'BEGIN{FS=":"} {printf "%0.2f\n", $3}' /etc/passwd
模式匹配 pattern
它与 sed
的 pattern
非常类似,都是按行进行匹配。
模式匹配的两种用法:
按正则表达式匹配。
运算符匹配。
正则匹配:
awk '/root/{print $0}' /etc/passwd
awk '/^lion/{print $0}' /etc/passwd
运算符匹配:
<=
小于等于
>=
大于等于
==
等于
!=
不等于
~
匹配正则表达式
!~
不匹配正则表达式
||
或
awk 'BEGIN{FS=":"}$3<50{print $0}' /etc/passwd
awk 'BEGIN{FS=":"}$3==1{print $0}' /etc/passwd
awk 'BEGIN{FS=":"}$7=="/bin/bash"{print $0}' /etc/passwd
awk 'BEGIN{FS=":"}$3~/[0-9]{3,}/{print $0}' /etc/passwd
awk 'BEGIN{FS=":"}$1=="lion" || $1=="frank" {print $0}' /etc/passwd
awk 'BEGIN{FS=":"}$3<50 && $4>50{print $0}' /etc/passwd
awk 中的表达式
awk 'BEGIN{var=20;var1="aaa";print var,var1}'
awk 'BEGIN{n1=20;n2+=n1;print n1,n2}'
awk 'BEGIN{n1=20;n2=30;printf "%0.2f\n",n1/n2}'
[注意] 在 BEGIN
和 END
中都可以正常执行表达式。
巩固习题:使用 awk 计算 /etc/services
中的空白行数量
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(条件表达式)
for(初始化计数器;计数器测试;计数器变更)
计算 1+2+3+...+100 的和
while
循环:
创建文件:while.awk
BEGIN{
while(i<=100){
sum+=i
print sum
执行命令:awk -f while.awk
for
循环:
创建文件:for.awk
BEGIN{
for(i=0;i<=100;i++)
sum+=i
print sum
执行命令:awk -f for.awk
do...while
循环:
创建文件:do-while.awk
BEGIN{
sum+=i
}while(i<=100)
print sum
执行命令:awk -f do-while.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")}'
tolower(str)
转换为小写字符
awk 'BEGIN{print tolower("I have a dream")}'
toupper(str)
转换为大写字符
awk 'BEGIN{print toupper("I have a dream")}'
split(str,arr,fs)
分隔符字符串,并保存到数组中
awk 'BEGIN{str="I have a dream";split(str,arr," ");for(a in arr) print arr[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]/)}'
substr(str,m,n)
截取子串,从 m
个字符开始,截取 n
位。若 n
不指定,则默认截取到字符串末尾。
awk 'BEGIN{str="I have a dream";print substr(str,4)}'
sub(RE,ReqStr,str)
替换查找到的第一个子串。
RE
为正则表达式
ReqStr
为要替换的字符串
str
为源字符串
awk 'BEGIN{str="I have 123 dream";sub(/[0-9]+/,"$",str);print str}'
gsub(RE,ReqStr,str)
替换查找到的所有子串
RE
为正则表达式
ReqStr
为要替换的字符串
str
为源字符串
awk 'BEGIN{str="I have 123 dream 456";gsub(/[0-9]+/,"$",str);print str}'
选项 option
-v
参数传递
-f
指定脚本文件
-F
指定分隔符
-V
查看 awk
的版本号
awk
的 BEGIN
中不能直接使用 shell
中定义的变量,需要通过 -v
参数进行传递
[root@lion ~]
[root@lion ~]
[root@lion ~]
20 30
指定脚本文件
'BEGIN{}pattern{commands}END{}'
这里的内容可以写在一个独立的文件中,通过 -f
参数来引入。
BEGIN{
var=30
print var
awk -f test.awk
这种写法更易于编写和维护,适合复杂 awk
语句。
指定分隔符
awk 'BEGIN{FS=":"}{print length($3)}' /etc/passwd
awk -F : '{print length($3)}' /etc/passwd
查看 awk 的版本号
[root@lion ~]
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
代码演示:
BEGIN{
printf "%-10s %-10s\n", "User", "Total Records"
USER[$6]+=$8
for(u in USER)
printf "%-10s %-10d\n", u , USER[u]
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 脚本(基础篇)
都看到这里了,就点个👍 👍 👍 吧。