Shell脚本编程

71 阅读6分钟

概述

image.png

查看该linux支持的是哪种shell

image.png

脚本入门

脚本以#!/bin/bash开头(指定解析器)

image.png

vim helloworld.sh

#!/bin/bash 
echo "helloworld"

bash helloworld.sh

image.png

image.png

分别使用sh bash ./ .的方式来执行shell脚本

前两种方式都是在当前shell中打开一个子shell来执行脚本内容,当脚本内容结束,则子shell关闭,回到父shell中。

第三种,也就是使用在脚本路径前加“.”或source的方式,可以使脚本内容在当前shell里执行,而无需打开子shell,这也是为什么每次修改完/etc/profile文件以后,需要source一下的原因。

开子shell与不开子shell的区别就在于,环境变量的继承关系,如在子shell中设置的当前变量,父shell是不可见的。

验证父子shell

image.png

变量

常用系统变量

$HOME、$PWD、$SHELL、$USER等

打印环境变量的值

echo $HOME

使用环境变量

ls $HOME

查看系统所有环境变量

env

或者

env | less

查看当前shell中所有变量(系统变量和用户自定义变量)

set

打印系统指定变量

printenv USER

自定义变量

1)基本语法

(1)定义变量:变量名=变量值,注意:=号前后不能有空格

(2)撤销变量:unset变量名

(3)声明静态变量:readonly变量,注意:不能unset

2)变量定义规则

(1)变量名称可以由字母、数字和下划线组成,但是不能以数字开头,环境变量名建议大写

(2)等号两侧不能有空格

(3)在bash中,变量默认类型都是字符串类型,无法直接进行数值运算

(4)变量的值如果有空格,需要使用双引号或单引号括起来

设置变量和修改变量

image.png

查看是系统变量还是自定义变量

下图开了子bash,在里面打印变量,没有相关的值,说明设置的变量不是全局变量

image.png

升级为全局变量

在子shell修改之后,export为全局变量,exit返回父shell,发现值还是没改变,说明子bash修改的变量作用范围只在子shell生效

image.png

shell脚本查看变量的可见范围

vim helloworld.sh

#!/bin/bash 
echo "helloworld" 
echo $my_aaa 
echo $my_new_var

chmod +x helloworld.sh

image.png

还有一种方式能在脚本里访问到局部变量,那就是在外面bash命令中,将变量升级为全局变量

export my_new_var

运算符

image.png

设置只读变量

image.png

撤销变量

image.png

任意路径执行脚本

查看环境变量PATH:echo $PATH

image.png

将shell脚本放到环境查询出来的环境变量的路径中,然后任意路径输入helloworld.sh即可执行

image.png

增加环境变量PATH的路径

vim /etc/profile

添加以下内容即可增加PATH的路径

export NODE_HOME=/usr/local/software/node-v22.6.0-linux-x64/bin

特殊变量

$n

$n(功能描述:n为数字,$0代表该脚本名称,$1-$9代表第一到第九个参数,十以上的参数需要用大括号包含,如\${10})

vim parameter.sh

#!/bin/bash 
echo '==============$n===============' 
echo script name: $0 
echo 1st parameter: $1 
echo 2nd parameter: $2

image.png

image.png

$#

$#(功能描述:获取所有输入参数个数,常用于循环,判断参数的个数是否正确以及加强脚本的健壮性)

vim paramter.sh

#!/bin/bash
echo '==============$n===============' 
echo script name: $(basename $0 .sh) 
echo script path: $(cd $(dirname $0); pwd)
echo 1st parameter: $1 
echo 2nd parameter: $2
echo '==============$#===============' 
echo parameter numbers: $#

image.png

\*和\@

$*(功能描述:这个变量代表命令行中所有的参数,$*把所有的参数看成是一个整体)

$@(功能描述:这个变量也代表命令行中所有的参数,不过$@把每个参数区分对待)

vim p2.sh

#!/bin/bash 
echo '================$*=================' 
echo $* 
echo '================$@=================' 
echo $@

image.png

$?

$?(功能描述:最后一次执行的命令的返回状态,如果这个变量的值为0,证明上一个命令正确执行;如果这个变量的值为非0(具体是哪个数,由命令自己来决定),则证明上一个命令执行不正确了。)

image.png

运算符

基本语法

"$((运算式))"或“$[运算式]”

expr计算,是把后面当成参数传入,所有每个数值和运算符之间都要加空格

image.png

expr赋值给变量

image.png

运算符计算

image.png

image.png

image.png

脚本计算

vim add.sh

#!/bin/bash 
sum=$[$1 + $2] 
echo sum=$sum

image.png

函数符

$(),用该符号包起来的内容会以函数的形式去运行

条件判断

1.基本语法

(1)test condition,如:test $a = hello(注意等号左右都要加空格),查看判断结果:echo $?,0为结果成功,1为结果失败

(2)[ condition ],如:[ $a = hello ](注意等号左右都要加空格),查看判断结果:echo $?,0为结果成功,1为结果失败

2.常用判断条件

(1)两个整数之间比较

-eq 等于(equal)

-ne 不等于(not equal)

-lt 小于(less than)

-le 小于等于(less equal)

-gt 大于(greater than)

-ge 大于等于(greater equal)

注:如果是字符串之间的比较,用等号“=”判断相等;用“!=”判断不等

[ 2 -lt 8 ]

echo $?

(2)按照文件权限进行判断

-r 有读的权限(read)

-w 有写的权限(write)

-x 有执行的权限(execute)

(3)按照文件类型进行判断

-e 文件存在(existence)

-f 文件存在并且是一个常规的文件(file)

-d 文件存在并且是一个目录(directory)

判断文件是否存在

[ -f /usr/local/aaa.txt ]

echo $?

判断文件是否存在的另一种写法

test -f /usr/local/aaa.txt

echo $?

字符串对比

字符串不加引号,加单引号,加双引号都可以

[ $a = hello ]

[ $a = 'hello' ]

[ $a = "hello" ]

image.png

image.png

image.png

image.png

数值对比

image.png

文件权限判断

image.png

文件类型判断

没有/home/info这个文件

image.png

多条件判断

$a是否小于20,是就打印$a<20,否就打印$a>=20

image.png

image.png

*流程控制

if判断

(1)单分支

if [ 条件判断式 ]; then 程序 fi

或者

if [ 条件判断式 ] then 程序 fi

(2)多分支

if [ 条件判断式 ] then 程序 else [ 条件判断式 ] then 程序 else 程序 fi

image.png

image.png

image.png

image.png

脚本

单分支

vim if_test.sh

下面x的意思是防止没有传入参数导致的空参报错

这里试了直接去掉x,判断式变量都带上双引号,执行脚本时没传入参数不会报错,而且执行逻辑正确

#!/bin/bash 
if [ "$1"x = "chenjt"x ] 
then 
echo "welcome,my big brother" 
fi

image.png

多分支

vim if_test.sh

#!/bin/bash 
if [ "$1" = "chenjt" ] 
then
    echo "welcome,my big brother" 
fi 

# 输入第二个参数,表示年龄,判断属于哪个年龄阶段 
if [ $2 -lt 18 ] 
then 
    echo "未成年人" 
elif [ $2 -lt 35 ] 
then 
    echo "青年人" 
elif [ $2 -lt 60 ] 
then 
    echo "中年人"
else 
fi

image.png

case语句

基本语法

case $变量名 in 
"值1") 
    如果变量的值等于值1,则执行程序1 
;; 
"值2") 
如果变量的值等于值2,则执行程序2 
;;
    _省略其他分支_ 
*) 
    如果变量的值都不是以上的值,则执行程序 
;; 
esac

注意事项:

(1)case行尾必须以单词"in",每一个模式匹配必须以右括号“)”结束。

(2)双分号“;;”表示命令序列结束,相当于java中的break。

(3)最后的"*)"表示默认模式,相当于java中的default。

vim case_test.sh

#!/bin/bash 
case $1 in 
1) 
    echo "one" 
;; 
2) 
    echo "two"
;; 
3) 
    echo "three" 
;; 
*) 
    echo "number else" 
;; 
esac

image.png

for循环

基本语法1

for(( 初始值;循环控制条件;变量变化 )) 
do 
    程序    
done

vim sum_to.sh

实操:从1加到100

#!/bin/bash 
for(( i=1; i<=$1; i++ )) 
do 
    sum=$[ $sum + $i ] 
done 
echo $sum

image.png

image.png

基本语法2

for 变量 in 值1 值2 值3... do 程序 done

遍历linux windows macos这三个值,打印出每一个值

image.png

{}:表示一个序列

image.png

test.sh


#!/bin/bash 
for param in $1 
do 
    echo $param
done

vim parameter_test.sh

$*和$@带双引号有区别,不带双引号无区别


#!/bin/bash 
 echo '============$*=============' 
 for para in "$*" 
 do 
 echo $para 
 done echo '============$@=============' 
 for para in "$@"
  do 
  echo $para
done

image.png

while循环

基本语法

while [ 条件判断式 ] do 程序 done

vim sum_to.sh

# while做一个实现 
a=1 
while [ $a -le $1 ] 
do 
    # sum2=$[ $sum2 + $a ] 
    # a=$[$a + 1] 
    let sum2+=a 
    let a++ 
done 
echo $sum2

image.png

read读取控制台输入

基本语法

read(选项)(参数)

1.选项

-p:指定读取值时的提示符

-t:指定读取值时等待时间(秒)如果-t不加表示一直等待

2.参数

变量:指定读取值的变量名

read_test.sh


#!/bin/bash 
read -t 10 -p "请输入您的芳名:" name 
echo "welcome, $name"

image.png

函数

系统函数

basename

基本语法

basename [string / pathname][suffix](功能描述:basename命令会删掉所有的前缀包括最后一个("/")字符,然后将字符串显示出来)。

basename可以理解为取路径里的文件名称

选项:

suffix为后缀,如果suffix被指定了basename会将pathname或string的suffix去掉

只是对路径去做切分,随便输入什么路径,不会去判断这个路径是否存在,只会取到路径最后的一个文件名

image.png

$()是用来做命令替换的,其等价于``(反引号)

vim cmd_test.sh

#!/bin/bash 
filename="$1"_log_$(date +%s) 
echo $filename

image.png

vim paramter.sh

$(basename $0 .sh):截取paramter.sh到.sh,结果:paramter

#!/bin/bash 
echo '==============$n===============' 
echo script name: $(basename $0 .sh) 
echo 1st parameter: $1 
echo 2nd parameter: $2
echo '==============$#===============' 
echo parameter numbers: $#

image.png

image.png

dirname

基本语法

dirname 文件绝对路径(功能描述:从给定的包含绝对路径的文件名中去除文件名(非目录的部分),然后返回剩下的路径(目录的部分))

dirname可以理解为取文件的绝对路径名称

只是对路径去做切分,随便输入什么路径,不会去判断这个路径是否存在

image.png

image.png

vim paramter.sh

保证了不管在什么路径下执行该脚本,都能打印出该脚本的绝对目录

分号用于分隔命令,以下命令等同于:cd /usr/local; pwd

echo script path: (cd(cd (dirname $0); pwd)

#!/bin/bash 
echo '==============$n===============' 
echo script name: $(basename $0 .sh) 
echo script path: $(cd $(dirname $0); pwd) 
echo 1st parameter: $1 
echo 2nd parameter: $2 
echo '==============$#===============' 
echo parameter numbers: $#

image.png

image.png

自定义函数

基本语法

[]:表示可选的意思

[ function ] funname[()] 
{ 
    Action; 
    [return int;] 
}

经验技巧

(1)必须在调用函数地方之前,先声明函数,shell脚本是逐行逐行运行,不会像其他语言一样先编译。

(2)函数返回值,只能通过$?系统变量获得,可以显示地加上:return返回,如果不加,将以最后一条命令运行结果,作为返回值。return后跟数值n(0-255)

vim fun_test.sh

sum=(add(add a b):这里重新赋值,是因为echob):这里重新赋值,是因为echo ?表示的范围只有0-255,超过255高位会被截掉,为了能计算超过255的值,这里采取重新赋值的方式

#!/bin/bash 
function add(){ 
    s=$[$1 + $2] 
    echo $s 
} 

read -p "请输入第一个整数:" a 
read -p "请输入第二个整数:" b 

sum=$(add $a $b)
echo "和:"$sum 
echo "和的平方:"$[$sum * $sum]

image.png

综合案例-文件归档

实际生产应用中,往往需要对重要数据进行归档备份

需求:实现一个每天对指定目录归档备份的脚本,输入一个目录名称(末尾不带/),将目录下所有文件按天归档保存,并将归档日期附加在归档文件名上,放在/root/archive下。

这里用到了归档命令:tar

后面可以加上-c选项表示归档,加上-z选项表示同时进行压缩,得到的文件后缀名为.tar.gz。

vim daily_archive.sh

-d 1:判断1:判断1是否是一个目录


#!/bin/bash 

# 首先判断输入参数个数是否为1 
if [ $# -ne 1 ] 
then 
    echo "参数个数错误!应该输入一个参数,作为归档目录名" 
    exit 
fi 

# 从参数中获取目录名称 
if [ -d $1 ] 
then 
    echo 
else 
    echo 
    echo "目录不存在!" 
    echo 
    exit 
fi 

DIR_NAME=$(basename $1) 
DIR_PATH=$(cd $(dirname $1); pwd) 

# 获取当前日期 
DATE=$(date +%y%m%d) 

# 定义生成的归档文件名称 
FILE=archive_${DIR_NAME}_$DATE.tar.gz 
DEST=/root/archive/$FILE 

# 开始归档目录文件 
echo "开始归档..." 
echo 

tar -czf $DEST $DIR_PATH/$DIR_NAME 

if [ $? -eq 0 ] 
then 
    echo 
    echo "归档成功!"
    echo "归档文件为:$DEST" 
    echo 
else 
    echo "归档出现问题!"
    echo 
fi 
exit

chmod u+x daily_archive.sh

mkdir /root/archive

归档/root/scripts这整个目录到/root/archive目录下,名字为archive_

image.png

脚本添加到定时任务

查看定时任务

crontab -l

编辑定时任务

crontab -e

每天凌晨2点执行备份脚本 daily_archive.sh


0 2 * * * /root/scripts/daily_archive.sh /root/scripts

正则表达式

正则表达式使用单个字符串来描述、匹配一系列复合某个语法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些符合某个模式的文本。在Linux中,grep,sed,awk等文本处理工具都支持通过正则表达式进行模式匹配。

常规匹配

image.png

常用特殊字符

image.png

^$:匹配空行

显示daily_archive.sh脚本中所有空行的行号

image.png

image.png

.*:匹配任意内容

image.png

image.png

image.png

image.png

匹配手机号

image.png

文本处理工具

cut

cut的工作就是“剪”,具体的说就是在文件中负责剪切数据用的。cut命令从文件的每一行剪切字节、字符和字段并将这些字节、字符和字段输出。

1.基本用法

cut [选项参数] filename

说明:默认分隔符是制表符

2.选项参数说明

-f:列号,提取到第几列

-d:分隔符,按照指定分隔符分割列,默认是制表符“\t”

-c:按字符进行切割后加个n表示取第几列 比如-c 3

vim cut.txt

dong shen
guan zhen
wo wo 
lai lai 
le le

image.png

image.png

image.png

image.png

image.png

image.png

image.png

awk

一个强大的文本分析工具,把文件逐行的读入,以空格为默认分隔符将每行切片,切开的部分再进行分析处理。

1.基本用法

awk [选项参数] '/pattern1/{action1} /pattern2/{action2}...' filename

pattern:表示awk在数据查找的内容,就是匹配模式

action:在找到匹配内容时所执行的一系列命令

2.选项参数说明

-F:指定输入文件分隔符

-v:赋值一个用户定义变量

3.内置变量

FILENAME:文件名

NR:已读的记录数(行号)

NF:浏览记录的域的个数(切割后,列的个数)

以“:”为分隔符分割内容,过滤出以root开头的内容,打印/etc/passwd第7列

image.png

以“:”为分隔符分割内容,过滤出以root开头的内容,打印第1、6、7列,之间以","拼接

image.png

只显示/etc/passwd的第1列和第7列,以逗号分割,且在所有行前面添加列名user,shell在最后一行添加”the last something“

BEGIN:在所有数据行在读取之前执行

END:在所有数据执行之后执行

image.png

将/etc/passwd文件中的用户id增加数值2并输出

image.png

将文件/etc/passwd以":"为分隔符,打印文件名,已读行号和每一行分割后列的个数

image.png

查询ifconfig命令输出结果中的空行所在的行号

image.png

打印出ifconfig所有网卡netmask之后带的ip

/netmask/:表示匹配有netmask这个字符串的这一行

image.png

image.png

综合案例-发送消息

我们可以利用Linux自带的mesg和write工具,向其他用户发送消息

需求:实现一个向某个用户快速发送消息的脚本,输入用户名作为第一个参数,后面直接跟要发送的消息。脚本需要检测用户是否登录在系统中、是否打开消息功能,以及当前发送消息是否为空。

当前登录的用户信息

who am i

image.png

当前所有登录用户信息

who

image.png

消息发送功能是否是打开的

mesg

image.png

查看所有当用消息发送功能是否是打开的(+号表示消息发送功能是打开的,pts/2表示连接的控制台)

who -T

image.png

关闭mesg

mesg n

开启mesg

mesg y

发送消息给指定的登录用户

pts/2表示连接的控制台标识

write chenjt pts/2

image.png

image.png

不交互式的写法

echo aa | write chenjt pts/2

image.png

image.png

-i:ignore case,忽略大小写

-m:max count,比如-m 1,表示如果有多个相同结果,表示只取第一个结果

vim send.sh


#!/bin/bash 
# 执行脚本带的参数,参数1:用户名,参数2:消息 

# 查看用户是否登录 
login_user=$(who | grep -i -m 1 $1 | awk '{print $1}') 

if [ -z $login_user ]
then 
    echo "$1 不在线!" 
    echo "脚本退出..." 
    exit 
fi 

# 查看用户是否开启消息功能 
is_allowed=$(who -T | grep -i -m 1 $1 | awk '{print $2}') 

if [ $is_allowed != '+' ] 
then 
    echo "$1 没有开启消息功能" 
    echo "脚本退出..." 
exit 
fi 

# 确认是否有消息发送
if [ -z $2 ] 
then 
    echo "没有消息发送" 
    echo "脚本退出..." 
    exit 
fi 

# 从参数中获取要发送的消息 
whole_msg=$(echo $* | cut -d " " -f 2-) 

# 获取用户登录的终端 
user_terminal=$(who | grep -i -m 1 $1 | awk '{print $2}') 

# 写入要发送的消息 
# 不交互式的写法 
echo $whole_msg | write $login_user $user_terminal 
#echo write $login_user $user_terminal 

if [ $? != 0 ] 
then 
    echo "发送失败!"
else
    echo "发送成功!" 
fi 
exit

image.png

image.png

image.png

路径检查-e-d-f-L-r-w-x-s-h

image.png

判断字符串长度-n-z

image.png

image.png