Shell脚本编程
一、简介
1、什么是shell
shell是操作系统和应用程序之间的命令解释器;
shell编程就是对一堆Linux命令的逻辑化处理 ;
2、shell的分类
- windows系统
命令行cmd.exe
- linux系统
shell有很多种,如sh,ksh,csh,bash,zsh等
在终端可输入
more /etc/shells
查看本机支持的shell
二、Linux常用命令
命令 | 备注 | |
---|---|---|
head | 默认获取前10行,常见选项:-n 指定行数;-c 指定显示字节数 | 打印前10行:head -n 10 test.txt;打印前2个字节:head -c 2 test.txt |
tail | 默认获取末尾10行 | |
cut | 获取某列内容,默认以规则的空格或tab键分割,常见选项:-d 指定分隔符;-f 指定某列 | |
uniq | 去重重复内容,常见选项:-d 仅显示重复内容;-c 显示重复次数 | |
sort | 对文本进行排序;默认以字符ASCII码数值从小到大排序,常见选项:-r 倒序;-n 以数值大小排序;-t 指定分隔符,默认空格;-knum 指定以某个字段排序 | 与uniq结合使用,先排序才能去重,因为uniq是相邻比较去重 |
wc | 计算文本数量,如wc-l test.txt |
示例A:head+tail
示例B:cut+head
1、获取以冒号为分隔符的第1列,第6列内容,并以逗号为分隔符输出;
2、显示结果的前3行;
示例C:sort+uniq
三、变量
1、变量分类
- 自定义变量:局部变量和全局变量
- 环境变量:Linux预定义的变量,如$PATH、$PWD$、HOME等
2、定义变量
利用等号“=”定义变量,如:变量名=变量值,等号两侧不能有空格;
- 方式一:变量名=变量值
变量值必须是一个整体,如需要存在空格等特殊字符,需要用方式二/三的双引号/单引号模式
- 方式二:变量名=’变量值‘
等号右侧为字符串,所见即所得(输出内容即为引号内容)
- 方式三:变量名=“变量值”
等号右侧为字符串,支持$var形式变量嵌套、 转义等!
- 方式四:变量名=$(linux命令)或用反引号
将命令执行后结果赋值给变量
$()与反引号相似,但优先级更高,并支持嵌套
- 方式五:变量名=(数组)、变量名=(`linux命令`)
数组中间使用空格隔开
上述变量默认是局部变量
示例A:将linux命令执行结果赋值给变量
#!/bin/bash
path=$(pwd)
files=`ls -al`
echo current path: $path
echo files: $files
以上2行和第3行分别演示了两种方式来将Linux命令执行结果保存到变量。
第2行将pwd执行结果(当前所在目录)赋值给path变量。
第3行将ls -al命令执行结果(列出当前目录下所有的文件及文件夹)赋值给变量
定义变量不用$符号,使用变量要加$
3、作用域
Shell 变量的作用域可以分为三种:
- 局部变量(local variable): 只能在函数内使用的变量 ;
- 全局变量(global variable): 在当前shell中任何地方使用的变量 ;
- 环境变量(environment variable): 可以在任何shell中使用的变量 。
3.1、局部变量
** 在 Shell 函数中定义的变量默认也是全局变量,它和在函数外部定义变量拥有一样的效果, 要想变量的作用域仅限于函数内部,可以在定义时加上local
命令,此时该变量就成了局部变量 **
#!/bin/bash
#定义函数
function func(){
a=99
}
#调用函数
func
#在函数外输出函数内部的变量
echo $a
输出结果:
99
#!/bin/bash
#定义函数
function func(){
local a=99
}
#调用函数
func
#输出函数内部的变量
echo $a
输出结果为空,表明变量 a 在函数外部无效,是一个局部变量
3.2、全局变量
所谓全局变量,就是指变量在当前的整个 Shell 进程中都有效。每个 Shell 进程都有自己的作用域,彼此之间互不影响
全局变量在不同 Shell 进程中有互不相关性,即在图形界面下同时打开两个 Shell,或使用两个终端远程连接到服务器(SSH) ,变量不通用; 就像小王家和小徐家都有一部电视机(变量名相同),但是同一时刻小王家和小徐家的电视中播放的节目可以是不同的(变量值不同);
需要强调的是,全局变量的作用范围是当前的 Shell 进程,而不是当前的 Shell 脚本文件,它们是不同的概念。打开一个 Shell 窗口就创建了一个 Shell 进程,打开多个 Shell 窗口就创建了多个 Shell 进程,每个 Shell 进程都是独立的,拥有不同的进程 ID。在一个 Shell 进程中可以使用 source 命令执行多个 Shell 脚本文件,此时全局变量在这些脚本文件中都有效;
在命令行定义的变量和脚本中定义的变量,相互间默认不通用,需要对脚本文件执行source命令,命令行可引用;多命令行执行export,脚本文件可引用;
3.3、环境变量
默认情况下变量的作用域是当前shell,如果用export命令将其导出,那么此变量在其所有的子shell中也生效,这种变量就是环境变量。环境变量只能向下传递,即父shell可以传递给子shell,反过来则不行。注意这里的环境变量不是变量在所有shell中都有效,而是在export变量时的shell的所有子shell中有效。
可以通过命令查看环境变量(只显示全局变量):env
- 定义环境变量
方法一:变量名=变量值;export 变量
方法二:export 变量名=变量值
[c.biancheng.net]$ a=22 #定义一个变量
[c.biancheng.net]$ echo $a #在当前Shell中输出a,成功
22
[c.biancheng.net]$ bash #进入Shell子进程
[c.biancheng.net]$ echo $a #在子进程中输出a,失败
[c.biancheng.net]$ exit #退出Shell子进程,返回上一级Shell
exit
[c.biancheng.net]$ export a #将a导出为环境变量
[c.biancheng.net]$ bash #重新进入Shell子进程
[c.biancheng.net]$ echo $a #在子进程中再次输出a,成功
22
[c.biancheng.net]$ exit #退出Shell子进程
exit
[c.biancheng.net]$ exit #退出父进程,结束整个Shell会话
可以发现,默认情况下,a 在 Shell 子进程中是无效的;使用 export 将 a 导出为环境变量后,在子进程中就可以使用了 。
我们一直强调的是环境变量在 Shell 子进程中有效,并没有说它在所有的 Shell 进程中都有效;如果你通过终端创建了一个新的 Shell 窗口,那它就不是当前 Shell 的子进程,环境变量对这个新的 Shell 进程仍然是无效的 。
通过 export 导出的环境变量只对当前 Shell 进程以及所有的子进程有效,如果最顶层的父进程被关闭了,那么环境变量也就随之消失了,其它的进程也就无法使用了,所以说环境变量也是临时的 ;
只有将变量写入 Shell 配置文件中才能达到这个目的!Shell 进程每次启动时都会执行配置文件中的代码做一些初始化工作,如果将变量放在配置文件中,那么每次启动进程都会定义这个变量 。
环境变量可用于定义shell的运行环境,环境变量可以在配置文件中定义与修改,也可以在命令行中设置,但是命令行中的修改操作在终端重启时就会丢失,因此最好在配置文件中修改(用户家目录的“.bash_profile“文件或者全局配置“/etc/profile”、“/etc/bashrc”文件或者“/etc/profile.d”文件中定义。)将环境变量放在profile文件中,每次用户登录时这些变量值将被初始化。比如HOME、USER、SHELL、UID等再用户登录之前就已经被/bin/login程序设置好了。
4、查看变量
方式一:echo ${变量名}
方式二:echo "$变量名",echo '$变量名'
5、特殊符号
符号 | 含义 | |
---|---|---|
$0 | 获取当前执行的shell脚本名,包括脚本路径 | |
$n | 获取当前执行的shell脚本第n个参数值,n=1-9,如果n大于9就要用大括号括起来${10} | |
$# | 获取当前shell命令行中参数的总个数 | |
$? | 获取执行上一个指令的返回值(0为成功,非0为失败) | 查看执行是否成功:echo $? |
$* | 获取当前执行的shell的所有参数,将所有的命令行参数视为单个字符串 | |
$@ | 个程序的所有参数"$1" "$2" "$3" "...",这是将参数传递给其它程序的最佳方式,因为它会保留所有内嵌在每个参数里的任何空白,将命令行的每个参数视为单个的字符串 | |
$$ | 获取当前的shell进程号 |
四、数值运算
支持基本的数据类型运算(+、-、*、/、%、==、!=、>、>=、<、<=)
在shell中,对于基本数据类型的运算主要分为两种,整数运算和浮点数(小数)运算
1、整数运算
方式一:$((算术表达式))
-------常用
表达式中变量可不加$,前后要加空格
方式二:expr 算术表达式
注意:
- 在乘法(*)中,我们需用反斜线()来转义,不然会报错。
- 运算符前后必须还有空格,否则会被直接当作字符串返回。
- 如果要将计算结果保存到变量,就需要用到我们上篇文章讲到的那两种方式($() 或者 ``)来替换命令了。
2、浮点数运算
需要借组其他组件,后续扩展
五、流程控制
1、条件表达式
1.1、返回值
- 条件成立,返回0
- 条件不处理,返回1
1.2、逻辑表达式
或(||)/且(&&)
1.3、文件比较
-f 判断输入内容是否是一个文件
-d 判断输入内容是否是一个目录
-x 判断输入内容是否可执行
-e 判断文件是否存在
1.4、数值比较
比较 | 描述 |
---|---|
n1 -eq n2 | 判断n1是否等于n2,等价于(( n1==n2 )) |
n1 -ge n2 | 判断n1是否大于或等于n2,等价于(( n1>=n2 )) |
n1 -gt n2 | 判断n1是否大于n2,等价于(( n1>n2 )) |
n1 -le n2 | 判断n1是否小于或等于n2,等价于(( n1<=n2 )) |
n1 -lt n2 | 判断n1是否小于n2,等价于(( n1<n2 )) |
n1 -ne n2 | 判断n1是否不等于n2,等价于(( n1!=n2 )) |
1.5、字符串比较
比较 | 描述 |
---|---|
str1 = str2 | 判断str1是否与str2相同 |
str1 != str2 | 判断str1是否与str2不相同 |
str1 < str2 | 判断str1是否比str2小(根据ASCII) |
str1 > str2 | 判断str1是否比str2大(根据ASCII) |
-n str1 | 判断str1的长度是否非0 |
-z str1 | 判断str1的长度是否为0 |
在使用大于(>)或小于(<)符号时,需要转义(>)(<),不然会把这两种符号时别为重定向,加双方括号可以不用转移,命令的格式如下:
[[ expression ]]
2、if
2.1 if-then语句
#写法一:
if <command>
then
<commands>
fi
#写法二:
if command; then
commands
fi
if语句后面接的是命令,但我们其它编程语言中,这儿都是接返回布尔值(true,false)的表达式
在shell脚本的if其实是根据紧跟后面的那个命令的退出状态码来判断是否执行then后面的语句的。
关于退出状态码,你只需要记住:正常退出(命令执行正常)的状态码是0, 非正常退出的状态码不是0(有不少)。
以上语句的语义为: 如果if后面的命令执行正常(状态码0),那么就执行then后面的语句。否则不执行。 fi代表if语句的结束。
2.2 if-then-else语句
#写法一:
if command
then
commands
else
commands
fi
#写法二:
if command1
then
commands
elif
command2
then
command3
fi
2.3 test语句
test命令用于if-then或者if-then-else语句中,主要用于判断列出的条件是否成立,如果成立,就会退出并返回退出状态码0,否则返回非0
直接用:
test condition
#或
[ expression ]
结合if-then语句用
if test condition
then
commands
fi
结合if-then-else语句用
if test condition
then
commands
else
commands
fi
条件成立就执行then语句,否则else语句
test命令只能判断一下三类条件:
- 数值比较
- 字符串比较
- 文件比较
总结:test 命令比较奇葩,>、<、== 只能用来比较字符串,不能用来比较数字,比较数字需要使用 -eq、-gt 等选项;不管是比较字符串还是数字,test 都不支持 >= 和 <=
3、for
#格式一
for 值 in 列表
do
执行命令
done
#格式二
max=10
for ((i=1;i<=max;i++))
do
echo ${i}
for...in循环默认是循环一组通过空格或制表符(tab键)或换行符(Enter键)分割的值。这个其实是由内部字段分隔符配置的,它是由系统环境变量IFS定义的
4、while
满足条件会一直循环
while 条件
do
执行语句
done
5、until
满足条件会一直循环
until 条件
do
执行语句
done
6、case
case 变量名 in
值1)
指令1
;;
值2)
指令2
;;
值3)
指令3
;;
....
*)
指令4
;;
esac
示例:命令行可进行算术运算
六、shell脚本格式
- 脚本首行加“#!/bin/bash”
- shell脚本文件后缀,建议命令为.sh,命名要简单;
- 脚本执行失败时,使用exit返回非零值,来退出程序;
- 默认缩进4个空格
- 单行注释:#
- 多行注释
:<<!
这是注释
!
七、函数
1、格式
#格式一:
函数名()
{
命令1
....
}
#格式二:
function 函数名
{
命令1
....
}
2、参数
Shell 中的函数在定义时不能指明参数,但是在调用时却可以传递参数
2.1 给脚本文件传递位置参数
2.2 函数调用时传递位置参数
可以理解成函数内部的$n为形参,函数调用时传递的参数为实参;
3、用户输入----read命令
read默认就是从键盘读取用户输入的数据;如果进行了重定向,那么可以从文件中读取数据
read [-options] [variables]
variables表示用来存储数据的变量,可以有一个,也可以有多个
options和variables都是可选的,如果没有提供变量名,那么读取的数据将存放到环境变量 REPLY 中
选项 | 说明 |
---|---|
-a array | 把读取的数据赋值给数组 array,从下标 0 开始。 |
-d delimiter | 用字符串 delimiter 指定读取结束的位置,而不是一个换行符(读取到的数据不包括 delimiter),其实只有-d后的第一个字符被作为结束的标志 |
-e | 在获取用户输入的时候,对功能键进行编码转换,不会直接显式功能键对应的字符。比如read -e -p "输入文件名:" str 执行后,输入文件名开头的几个字符,使用tab键可以进行文件名补全(文件在当前目录存在) |
-n num | 读取 num 个字符,而不是整行字符。空格也算是一个字符 |
-p prompt | 显示提示信息,提示内容为 prompt。 |
-r | 原样读取(Raw mode),不把反斜杠字符解释为转义字符。 |
-s | 静默模式(Silent mode),不会在屏幕上显示输入的字符。当输入密码和其它确认信息的时候,这是很有必要的。 |
-t seconds | 设置超时时间,单位为秒。如果用户没有在指定时间内输入完成,那么 read 将会返回一个非 0 的退出状态,表示读取失败。 |
-u fd | 使用文件描述符 fd 作为输入源,而不是标准输入,类似于重定向。 |
功能 | 脚本 | 结果 | 注意点 |
---|---|---|---|
给多个变量赋值 | #!/bin/bash read -p "please input name and age:" name age echo "名字为${name}" echo "年龄为${age}" |
运行结果: please input name and age:emma 17 名字为emma 年龄为17 |
1.必须在一行内输入所有的值,不能换行,否则只能给第一个变量赋值,后续变量都会赋值失败 2.使用了 -p 选项,该选项会用一段文本来提示用户输入3.脚本中-p后的提示字符与变量name之间必须有一个空格,否则变量name会被当成提示信息的一部分 |
只读取一个字符 | #!/bin/bash read -n 1 -p "Enter a char > " char printf " " #换行 echo $char |
运行结果: Enter a char > 1 1 不使用printf " "时的运行结果为 Enter a char > 11 |
1.-n 1 表示只读取一个字符。运行脚本后,只要用户输入一个字符,立即读取结束,不用等待用户按下回车键。 2.printf "
" 语句用来达到换行的效果,否则 echo 的输出结果会和用户输入的内容位于同一行,不容易区分。 |
在指定时间内输入密码 | #!/bin/bash if read -t 20 -sp "Enter password in 20 seconds(once) > " pass1 && printf " " && #第一次输入密码 read -t 20 -sp "Enter password in 20 seconds(again)> " pass2 && printf " " && #第二次输入密码 [ $pass1 == $pass2 ] #判断两次输入的密码是否相等 then echo "Valid password" else echo "Invalid password" fi |
如果两次输入密码相同,运行结果为: Enter password in 20 seconds(once) > Enter password in 20 seconds(again)> Valid password 如果两次输入密码不同,运行结果为: Enter password in 20 seconds(once) > Enter password in 20 seconds(again)> Invalid password 如果第一次输入超时,运行结果为: Enter password in 20 seconds(once) > Invalid password 如果第二次输入超时,运行结果为: Enter password in 20 seconds(once) > Enter password in 20 seconds(again)> Invalid password |
1.&& 组合多个命令时,这些命令会依次执行,但只要一个命令失败,后续命令都不会执行 |
把读取的数据赋值给一个数组 | #!/bin/bash read -a test_arr -p 'please input some values:' echo 'the num of the input values:'${#test_arr[]} for item in ${test_arr[]} do echo ${item} done |
please input some values:aa bb cc dd the num of the input values:4 aa bb cc dd |
|
-d | read -d delimiter -p 'please input data>' test_var 输出:please input data>test432decho $test_var 输出:test432 |
1.使用-d指定分隔符后,只有-d后的第一个字符被作为结束标志 | |
等待输出q退出 | read -dq -p "Input some words end with q:" word | #输入,直到输入q,将自动退出 | |
-e | read -e -p "please input filename:" str 输出:please input filename:learn_ #按tab键 learn_arr.sh learn_cmd.sh learn_read1.sh learn_read.sh learn_spe_var.sh learn_str.sh #显示出所有相关的文件 继续输出:please input filename:learn_ |
||
读文件中的内容 | #!/bin/bash num=1 cat learn_read1.sh|while read line do echo "current line:$num--$line" num=$[ $num+1 ] done echo 'finish' exit 0 |
current line:1--#!/bin/bash current line:2--read -d 'rrr' -p 'pleass input some data>' name current line:3--echo '' current line:4--echo $name finish |
|
-u使用文件描述符 fd 作为输入源,而不是标准输入 | #将afile文件中的前三行与bfile中的前四行拼接在一起 while read -u3 i && read -u4 j;do echo $i $j done 3<afile 4<bfile |
read -u3 i 的意思是从 3 号 fd (file descriptor,文件描述符) 中读一行数据到 i 变量中,而 3<afile 的意思是重定向 afile 到 3 号 fd 中 所以,整个代码的意思是,不断从 afile 和 bfile 中分别读取内容到i , j 中,然后用echo 打印出来。 这个循环会一直执行直到遇到 afile 或 bfile 中至少任意一个的文件尾。 |