shell脚本学习总结
简介
本文会全面介绍shell脚本的基础知识。
脚本格式
要把shell命令放到一个“脚本”当中,有一个要求:脚本的第一行必须写成类似这样的格式:
#!/bin/bash
bash是一个shell解释器,用来解释shell命令。
我们先来写一个最简单的shell脚本,文件命名为1.sh:
#!/bin/bash echo hello!
如果我们的系统使用的是其他的解释器,就要将/bin/bash修改成相应的名字。
注意:脚本文件默认是没有执行权限的,想执行这个脚本必须给它添加权限:
zzc@zzc-virtual-machine:~/share/example$ ./1.sh -bash: ./1.sh: 权限不够 zzc@zzc-virtual-machine:~/share/example$ chmod +x 1.sh zzc@zzc-virtual-machine:~/share/example$ ./1.sh hello!
变量
shell脚本语言是一种弱类型语言,在脚本当中使用变量不需要也无法指定变量的“类型”。
默认状态下,shell脚本的变量都是字符串,即一连串的单词列表。
(1)变量的定义和赋值
myname=hello kitty
注意:赋值号两边没有空格;在shell脚本中,任何时候给变量赋值,赋值号两边一定不能有空格。
另外,变量名也有类似于C语言那样的规定:只能包含英文字母和数字,且不能以数字开头。
(2)变量的引用
在变量前面加上一个美元符号,表示对变量进行引用:
$myname
zzc@zzc-virtual-machine:~/share/example$ myname=hello kitty zzc@zzc-virtual-machine:~/share/example$ echo $myname hello kitty
(3)变量的种类
shell脚本中有如下几种变量。
- 普通的用户自定义变量,如上面的myname
- 系统预定义好的环境变量,如PATH
- 命令行变量,如 $#、$*。
如何查看系统的环境变量?
可以输入以下命令:
zzc@zzc-virtual-machine:~/share/example$ env SHELL=/bin/bash WINDOWID=8388615 QT_ACCESSIBILITY=1 COLORTERM=truecolor XDG_CONFIG_DIRS=/etc/xdg/xdg-ukui:/etc/xdg XDG_SESSION_PATH=/org/freedesktop/DisplayManager/Session0 GTK_IM_MODULE=fcitx LANGUAGE=zh_CN:en_US:en QT_AUTO_SCRENN_SET_FACTOR=0 ......
如何设置环境变量?
以PATH为例,如果想要将其修改为dir/,可以这么做:
export PATH=dir/
PATH环境变量的作用是保存系统中可执行程序和shell命令的所在路径,因此他的值都是一些以分号隔开的目录。
我们经常这么用:
export PATH=$PATH:dir/
这样做不会改变它原来的值,只是增加了一个目录。
还有,这样做只会在当前的shell中临时有效,如果想要永久有效,就必须把命令export PATH=$PATH:dir/写入~/.bashrc中,然后执行以下命令使之生效:
source ~/.bashrc
而shell脚本中的所谓命令行变量,是指在脚本内部使用用户从命令行中传递进来的参数,如:
./1.sh abcd 1234
我们在执行1.sh时给它传递了两个参数,分别是abcd和1234,要访问这两个参数,就必须使用命令行变量。
下面列出所有的命令行变量及含义:
命令行变量 | 含义 |
---|---|
$# | 命令行参数个数 |
$* | 表示所有参数 |
$@ | 表示所有参数 |
$n | 第n个参数 |
$? | 代表最后一个命令执行之后的返回值 |
$$ | 表示当前shell的进程PID |
注:$符号后面跟着的就是命令行变量
特殊符号
shell脚本有几种特殊符号:引号、竖杠(管道)、大于号和小于号(重定向)。
引号
引号有三种,他们是双引号 、单引号' '、反引号` `。
(1)双引号的作用是把一些“单词”括起来形成单个的“值”,比如:
myname=lady gaga
1、双引号可以包含对变量的引用,比如:
fruit=apple mytree=$fruit tree
由于对变量进行了引用,所以mytree的最终值是apple tree。
2、双引号也可以包含一个命令,比如:
today=today is `date`
这里date是一个shell命令,用来获取系统的当前时间,默认情况下脚本会把它当成一个普通的单词,所以需要用反引号(``)来让脚本识别他是一个命令。
(2)单引号
如果一个字符串被单引号包含,那么其内部的所有成分都被视为普通的字符。如下:
var='$myname, today: `date`'
这个变量var的值不会引用myname,也不会执行date命令。
(3)反引号
他用来在双引号中标识出命令。
竖杠(管道)
我们常常需要将一个命令达成的结果给到另外一个命令进行再加工,这时候就需要用到管道,例如:
zzc@zzc-virtual-machine:~/share/example$ ls -l | wc 15 128 695
管道就像水管一样,把前面命令的执行结果输送给后面的命令。
管道不仅可以连接2个命令,还可以连接多个命令,如下:
cat /etc/passwd | awk -F/ '{print $1}' | wc
大于号> 、小于号<(重定向)
每一个进程在刚开始运行的时候系统会默认为他们打开3个文件:标准输入、标准输出、标准出错,如下图1-30所示。
![2](https://www.icode9.com/i/ll/?i=418ac55046944d08808c26ba4f67c2cf.png =300x
)
当我们打开普通文件(a.txt,b.doc)之后,系统也会为这些文件产生文件描述符,如下图1-31:
1、我们可以使用重定向符号对标准输入和输出进行重定向,比如把ls命令的成功的输出结果重定向到a.txt文件中去,方法如下:
ls 1> a.txt
原理如下图所示:
2、我们也可以把ls命令的失败的输出结果重定向到a.txt文件,方法如下:
ls 2> a.txt
原理如下图:
3、我们把标准输入重定向到b.doc,方法如下:
echo 0< b.doc
原理如下图:
另外,在shell脚本中,在重定向符的右边,标准输入/输出设备文件描述符要写成&0,&1,&2;
比如,把一句话输出到标准出错设备中去:
echo hello world 1>&2
字符串处理
shell中对字符串的处理,可以使用awk、sed这些神器,有兴趣的朋友请自行查阅相关资料。
下面介绍一些简单场合下的办法。
1、计算一个字符串的字符个数:
zzc@zzc-virtual-machine:~/share/example$ var=apple tree zzc@zzc-virtual-machine:~/share/example$ echo ${#var} 10
2、删除一个字符串左边部分字符
zzc@zzc-virtual-machine:~/share/example$ path=/etc/rc0.d/K20openbsd-inetd zzc@zzc-virtual-machine:~/share/example$ level=${path#/etc/rc[0-9].d/[SK]} zzc@zzc-virtual-machine:~/share/example$ echo $level 20openbsd-inetd
3、删除一个字符串右边部分的字符
zzc@zzc-virtual-machine:~/share/example$ path=/etc/rc0.d/K20openbsd-inetd zzc@zzc-virtual-machine:~/share/example$ level=${path#/etc/rc[0-9].d/[SK]} zzc@zzc-virtual-machine:~/share/example$ level=${level%%[a-zA-Z]*} zzc@zzc-virtual-machine:~/share/example$ echo $level 20
测试语句
test命令专门用来实现所谓的测试语句,用于检查某个条件是否成立,它可以进行数值、字符和文件三个方面的测试。
1、数值测试
参数 | 说明 |
---|---|
-eq | 等于则为真 |
-ne | 不等于则为真 |
-gt | 大于则为真 |
-ge | 大于等于则为真 |
-lt | 小于则为真 |
-le | 小于等于则为真 |
示例如下:
#!/bin/bash num1=100 num2=100 if test $[num1] -eq $[num2] then echo '两个数相等!' else echo '两个数不相等!' fi
运行结果如下:
zzc@zzc-virtual-machine:~/share/example$ ./1.sh 两个数相等!
2、字符串测试
参数 | 说明 |
---|---|
= | 等于则为真 |
!= | 不相等则为真 |
-z 字符串 | 字符串的长度为零则为真 |
-n 字符串 | 字符串的长度不为零则为真 |
示例如下:
#!/bin/bash num1=ru1noob num2=runoob if test $num1 = $num2 then echo '两个字符串相等!' else echo '两个字符串不相等!' fi
运行结果如下:
zzc@zzc-virtual-machine:~/share/example$ ./1.sh 两个字符串不相等!
3、文件测试
参数 | 说明 |
---|---|
-e 文件名 | 文件存在则为真 |
-r 文件名 | 如果文件存在且可读则为真 |
-w 文件名 | 如果文件存在且可写则为真 |
-x 文件名 | 如果文件存在且可执行则为真 |
-s 文件名 | 如果文件存在且至少有一个字符则为真 |
-d 文件名 | 如果文件存在且为目录则为真 |
-f 文件名 | 如果文件存在且为普通文件则为真 |
-c 文件名 | 如果文件存在且为字符型特殊文件则为真 |
-b 文件名 | 如果文件存在且为块特殊文件则为真 |
示例如下:
#!/bin/bash cd /bin if test -e ./bash then echo '文件已存在!' else echo '文件不存在!' fi
运行结果如下:
zzc@zzc-virtual-machine:~/share/example$ ./1.sh 文件已存在!
另外,shell脚本也可以使用方括号来代替test语句,看起来更顺眼:
#!/bin/bash if [ -e 1.c ] && [ -r 1.c ] then cat 1.c fi
注意:括号左右两边必须有空格
脚本语法单元
与C语言很类似,shell脚本也需要一套基本单元来控制整个逻辑的执行,包括所谓的控制流(分支控制和循环控制)、函数、数值处理等。
分支控制
示例如下:
if [ -e file ] && [ -r file ] then cat file fi
语法要点如下:
(1)每一个if语句都有一个fi(if倒过来写)作为结束标记
(2)分支结构中使用then作为起始语句
(3)当且仅当if后面的语句执行结果为真时,then以下的语句才会被执行;if也可以跟else配对使用,如下所示:
#!/bin/bash if [ -e 1.c ] && [ -r 1.c ] then cat 1.c #如果文件存在且可读,则显示文件内容 elif [ -e 1.c ] then chmod u+r 1.c #如果文件存在且不可读,就加读权限再显示其内容 cat 1.c else touch 1.c #如果文件不存在就创建该空文件 fi
如果是多路分支,可以使用case语句。
例如,要实现这么一个功能:要求用户输入一个数字,判断如果输入的是1,则输出one;如果输入的是2,则输出two;输入其他数字就输出unknown。
示例如下:
#!/bin/bash read VAR # 从键盘输入一个数字 case $VAR in # 判断用户输入的值$VAR 1) echo one #如果$VAR的值是1,就显示one ;; 2) echo two #如果$VAR的值是2,就显示two ;; *) echo unknown # 星号*是shell中的通配符,代表任意字符 esac
注意:
1、变量VAR的值实际上是字符串,因此上述代码中的1可以写成1。
2、整个case结构必须以esac作为结束标记。
循环控制
shell脚本中有三种可用的循环结构,他们分别是while循环、until循环和for循环。
先来看while循环和until循环,假设现在要实现打印1~100的功能,分别用这两种语句实现,如下:
- while循环语句:
#!/bin/bash declare -i n=1 # 在定义变量n前面加上declare -i表示该变量是数值 while [ $n -le 100 ] # 如果变量n的值小于等于100,则循环 do # 循环体用do和done包含起来 echo $n n=$n+1 #使n的值加1 done
- until循环语句
#!/bin/bash declare -i n=1 # 在定义变量n前面加上declare -i表示该变量是数值 until [ $n -gt 100 ] # 如果变量n的值大于100,则退出循环 do # 循环体用do和done包含起来 echo $n n=$n+1 #使n的值加1 done
下面再来看for循环,假设现在要实现:列出当前目录下每个普通文件所包含的行数。
示例代码如下:
#/bin/bash files=`ls` # 在当前目录下执行ls,将所有文件名保存在变量files中 for a in $files #循环地将files里面的每个单词付给a do if [ -f $a ] #如果文件$a是一个普通文件,就计算他的行数 then wc -l $a fi done
注意,for循环中,in后面接的是一个字符串,字符串里面包含几个单词循环体就执行几遍,每执行一遍a的值都轮换地等于字符串里边的各个单词。
函数
shell脚本有些时候也可以编写模块化代码,将具有某一特定功能的代码封装起来,以供别处调用。
比如,编写一个可以检测某用户是否在线的函数,如下所示:
#/bin/bash check_user() #定义一个函数,注意括号里没有空格 { if [ $1 = quit ] #若函数的第一个参数$1是quit,就立即结束脚本 then exit fi USER=`who | grep $1 | wc -l` if [ $USER -eq 0 ] #判断用户是否在线,在线返回1,不在线返回0 then return 0 else return 1 fi } while true do echo -n input a user name: read USERNAME check_user $USERNAME if [ $? -eq 1 ] then echo [$USERNAME] online. else echo [$USERNAME] offline. fi done
注意几点:
1、函数的定义中,括号里不能写任何东西
2、函数必须定义在调用之前
3、给函数传参时,在函数定义里用$n来代表第n个参数,如果是第10个或之后的参数,必须用${10}表示
4、$?表示函数调用的返回值
trap
当脚本收到某个信号时,需要处理一些清理工作,然后再退出,类似于POSIX编程中的信号处理。脚本中使用trap来达到这个目的。
trap INT
上面语句的含义是:当脚本收到信号SIGINT时,忽略该信号。在LINUX中所支持的信号可以使用命令trap -l来查看。信号名称的前缀要省略。trap除了可以“忽略”信号,也可以捕获信号,例如:
trap do_something INT QUIT HUP
这句话意思是当脚本收到INT、QUIT、HUP信号时执行函数do_something。
此处还可以指定脚本正常退出时的默认动作,例如:
trap on_exit EXIT
这句话是指当脚本正常退出时,执行函数on_exit。
有的时候,当shell脚本收到某个信号时,我们需要立即终止脚本,且要执行正常退出时的清理函数,该情况下可以这么做:
trap on_exit EXIT trap : INT HUP
以上两句的意思是当脚本正常退出时调用函数on_exit,当脚本收到信号INT或HUP时执行空指令(此处冒号代表一个空指令,如果没有冒号,脚本将完全忽略该信号,不做响应,不能立即退出),完毕后正常退出,此时触发EXIT从而执行函数on_exit。
常见问题
语法问题
- 变量赋值,等号左右有空格
- if结构没有以fi结尾
- 循环结构没有do或者没有done
- if语句方括号左右没有空格
逻辑问题
- 使用变量时,没有引用变量
权限问题
- 无法执行shell脚本,没有执行权限
总结
本篇文章对个人最近学习的shell脚本知识和所做的实践进行了总结,到这里也算是入门了;后面有时间再扩充相关的知识点和一些可能遇到的问题。