Shell 脚本基本知识小结
1. shell 脚本简介
1.1 什么是 shell
通俗一点的解释就是命令解析器,用以接收用户输入的命令,然后调用相应的应用程序.
Shell 是用户和UNIX/Linux
操作系统内核程序间的一个接口.
Shell 的种类有Bourne Shell(sh)
,Bourne Again Shell(bash)
,C Shell(csh)
,Korn Shell(ksh)
,Linux
标准shell
为bash
,也支持C Shell
和Korn Shell
.
要注意我们敲命令的地方(也就是那个黑色框框)不叫 Shell!!!那个东西叫 terminal,我们在 terminal 中输入命令,然后 Shell 接收指令. Shell 可看成是一个进程,terminal 是这个进程的输入输出,与用户交互的部分.
详细可看知乎:终端、Shell、tty 和控制台(console)有什么区别?
1.2 什么是脚本文件
脚本文件是普通的文本文件,但该文件中是一条条的指令,操作系统在执行该脚本文件的时候,会用一种解释器(bash、Python 等)来逐行解释执行该文件中的指令. 操作系统使用什么解释器来执行该脚本文件,是需要人为来指定的(当不指定时,使用不同的 shell 会有不同的策略). 这种指定可以是这样的:
bash test.sh
表示使用 bash 解释器来解释执行 test.sh,大家运行 Python 文件的时候使用python test.py
,跟这个道理是相同的. (请始终注意,文件拓展名大多数情况下都是给人看的,操作系统不会自动把.py
结尾的文件看做是 Python 文件,你同样可以把一个 bash 脚本保存成test.py
这样的名称,只不过这容易给人造成误解而已).
还有一种指定解释器的方法,就是在脚本文件的第一行通过注释的方式指明所使用的解释器. 就像下面这样:
#!/bin/bash
echo 'Linux is cool!'
这样,在执行该文件的时候,就不需要手动指定解释器了,只需要输入该文件的名称就可以了. 但一般这个时候执行这个文件会报错,类似于:
-bash: ./test.sh: Permission denied
咦,明明是我创建的这个文件,我再来执行它为啥会“Permission denied(没有权限)”呢?这时候查看一下这个脚本文件的权限:
-rw-rw-r-- 1 loheagn loheagn 27 Apr 28 21:33 test.sh
发现了吧?该文件确实没有可执行的权限.
这时,我们使用chmod
来修改文件权限就可以了,例如可以:
chmod 764 test.sh
或者直接:
chmod +x test.sh
1.3 运行第一个 shell 脚本
以helloworld.sh
为例
#!/bin/bash # 这一行称为shebang
echo "Hello, World!"
$bash hellowrold.sh
直接运行,显示调用shell
来解释脚本将忽略 shebang 的指定解释器.- 运行该程序需要具有可执行权限,可通过
chmod +x helloworld.sh
进行赋权../helloworld.sh
即可看到输出. - 处于安全考虑,当前目录并没有被加在用户的
$PATH
变量,因此不能这样调用scripname
. #
为注释符号.#!/bin/bash
被称为 shebang. 具体其来源与作用见释伴:Linux 上的 Shebang 符号(#!).#!/bin/bash
指定解释器,若忽略,则无法使用shell内建命令;在脚本行中则只是注释.#!/bin/rm
会自己删除自己;#!/bin/more
打印自己.
1.4 命令连接符
命令的执行是串行的,一条命令结束才能输入下一条命令,可以在命令之间加上;
分割命令,从而可以一行输入所有命令. Shell 会挨个执行.
&&
连接符- 命令
1 && 命令2 && 命令3
,Shell 在判断出这个表达式的真假后就会停止执行. 如果命令 1 为false
,可以判断表达式 一定为假,执行停止. 如果为true
,那么还需要执行命令 2,一直执行到能判断真假为止或者执行完被&&
连接的命令.
- 命令
||
连接符- 同
&&
, 执行到能判断真假或者所有被连接命令被执行完为止.&&
和||
的计算方式同 c 语言中的&& ||
. 以上运算原则又被称作短路原则
- 同
1.5 exit
exit
命令被用来结束脚本,返回状态码给shell
exit n
,当n
为 0 时,表示执行成果,非 0 表示一个错误码.$?
读取上一条命令的退出码- 脚本中若无
exit
语句,其返回状态将由最后一条语句的执行状态决定 - 特定的退出吗都有预定的含义,用户不因该在自己的脚本中使用它. 如 126 表示文件不可执行,127 表示命令未找到.
#!/bin/bash
echo hello
echo $? # 返回 0 ,因为执行成功
xxx
echo $? # 返回非 0 ,因为失败了
exit 123 # 将返回 123 给 shell
2. shell 变量
shell
变量不分数据类型shell
变量依赖上下文shell
允许比较操作和算术操作,关键在于变量的值中是否只有数字
2.1 环境变量
2.1.1 什么是环境变量
环境变量可以简单理解为,在程序运行所处的环境中,提前设定好的参数值. 程序在执行过程中,会去读取这些提前设定好的参数值.
举个例子,大家在 Windows 中安装完 jdk 时,双击安装完安装包后,都要去修改JAVA_HOME
、CLASSPATH
、PATH
这三个环境变量. 其中当时这个JAVA_HOME
我们设定的是一个目录,这样,当有软件需要使用 jdk 的时候,它就会尝试读取这个JAVA_HOME
的值,从而知道系统安装的 jdk 在哪里.
Linux 默认存在的环境变量有PATH
、HOME
等,我们可以通过echo $PATH $HOME
查看. 若要查看所有的环境变量,可以直接使用$ env
命令.
有没有发现对环境变量的引用与我们在 shell 脚本中引用变量的方法完全一样?因为它本质上就是已经提前定义好的变量嘛!
常用的环境变量:
环境变量名 | 含义 |
---|---|
HOME | 用于保持注册目录的完全路径名 |
PATH | shell按照该变量的顺序搜索与名称一致的可执行文件 |
TERM | 终端类型 |
UID | 当前用户的标识 |
PWD | 当前工作目录的绝对路径 |
PS1 | 主提示符,特权用户是#,普通用户是$ |
SHELL | 用户当前使用的shell. 它也指出你的shell解释程序放在什么地方. |
USER | 当前用户名 |
2.1.2 设定或修改环境变量
在 Windows 中,我们一般是直接使用 GUI 界面修改,然后重启就可以了. 但在 Linux 必须使用命令行操作.
总的来说,在 Linux 中设定环境变量的语法很简单:
export environment_variable=xxxxx
但是你会发现,当你退出当前的 Terminal,再次打开一个新的 Terminal 时,将无法再次访问system_programming
. 这是因为,我们上次进行的修改,是在一个 bash 的进程中修改的,当我们关闭 Terminal,就终止了这个进程;当再次启动一个新的 Terminal 时,就重新开启了一个新的进程,这个新的进程自然是访问不到别的进程的变量的. (关于进程的概念,后续学习中会有介绍).
那该如何“永久”设置环境变量呢?我们知道,当一个 bash 进程启动时(即,打开一个 Terminal 或者远程 SSH 登录时),该进程会读取~/.bashrc
文件来完成初始化. 因此,我们只需要把上面提到的export
语句写到~/.bashrc
文件中就可以了. 注意,该文件前面加了.
,也就是说这是一个隐藏文件,要使用ls -a
才能看到.
当然,这种方法只能使得当前用户,也就是我们自己访问到该环境变量,对于 root 用户,或者其他注册用户来讲,是访问不到的. 为了达到所有用户都可以访问的效果,我们可以把export
语句写到/etc/profile
文件,当系统启动时会读取到该文件.
2.1.3 PATH 环境变量
当大家在工作目录中使用 GCC 编译出来了一个可执行文件a.out
,要运行这个程序的时候是这样进行的:
./a.out
直接在 shell 中输入了要执行的文件的文件名. 而且正如上一节所看到的,执行脚本文件也是直接输入文件名. 这是在 bash shell 中执行可执行文件的唯一方式.
但如果我们不在这个可执行文件所在的目录下怎么办?如果我在一个其他目录下,想执行这个程序,就需要用相对路径或绝对路径写出整个路径前缀,这往往是一件非常麻烦的事情. 这时候,PATH
就诞生了.
PATH
是一个环境变量,这个环境变量指明了系统默认的查找可执行文件的路径. 你可以在 bash shell 中使用echo $PATH
打印出你当前的PATH
,它将如下所示:
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
这一行输出实际上是几个路径之间用:
拼接起来的. 出于安全考虑 当前目录并没有被加在用户的 $PATH
变量中 . 因此 在当前目录下调用脚本 ./scriptname
这种形式.
有了PATH
,当你在命令行输入一个程序名时,bash shell 就会去PATH
所指定的这几个目录中去寻找该程序,如果找不到就会报错.
现在大家知道了吧,我们平常使用的指令ls
、mv
等指令没啥神奇的,他们只不过是操作系统预设好的一个个程序,并放在了PATH
指定的某个路径下,我们输入指令时,其实就相当于是在执行这些程序.
比如,我们可以用which
来查看这些“内置程序”的具体路径
这一点可以去路径/usr/bin
下去查看验证一下.
2.2 用户变量
- 在 Shell 中使用变量无需定义,在使用的时候创建. 并且变量不分类型,Shell 统一认为是字符串,需要的时候通过一些命令进行转换.
- 变量赋值: 变量名=值 ,等号左右不能够有空格. 若字符串中包含空格,则需要用单/双引号括起来.
- 可以使用
readonly
将变量改为只读类型. - 通过
$
引用变量值,echo $SHELL
. - 输入变量,
read
变量名.
操作 | 用法 |
---|---|
初始化 | varName=varValue |
只读 | readonly varName |
普通变量调用 | $varName |
命令变量调用 | $(commad) | `comand` (二者等效,推荐后者,作用是在调用的位置替换为命令结果) |
- 引号的使用
引号 | 用法 |
---|---|
双引号:"" | 双引号括起来的字符 除$,\,'," 都将作为普通字符对待 |
单引号:'' | 均作为普通字符出现 |
反引号:```````` | 命令结果替换, 等效于 $(command) |
下面介绍更详细介绍几种常用的变量.
2.2.1 字符串
以下是字符串常用的操作,涉及道正则表达式的知识可以自行搜寻。
- 字符串长度
长度: echo `expr length $str` | ${#str}
find: echo `expr index $str $tofind` 匹配到子串的第一个字符出现的位置
匹配长度: echo `expr $str : 'abc[A-Z]*'` | `expr mathc $str 'abc[A-Z]*'`
匹配子串: echo `expr $str : '\(abc[A-Z]*\)'` | `expr mathc $str '\(abc[A-Z]*\)'`
提取子串: ${str:position:length} | echo `expr substr $str position length`
删去从第一个字符开始的子串: 第一个匹配${str#substr} | 最后一个匹配${str##substr}
删去从最后的字符结束的子串: 第一个匹配${str%substr} | 最后一个匹配${str%%substr}
子串替换:第一个 ${string/substring/replacement} | 全部 ${string//substring/replacement}
开头部分匹配${string/#substring/replacement} | 结尾部分匹配${string/#substring/replacement} 后两者不推荐
2.2.2 运算变量
算术替换(三者等效)
$((1+1)) # 推荐
$(expr $x + 1) # expr 注意空格!
`expr $x + 1`
# 例子
i=1
((i+=1))
ecoh $i # 2
区别:
调用 | 含义 |
---|---|
$(command) | $`command` | 命令替换 |
$((expression)) | $(expr expression) | `expr expression` | 算术命令替换 |
((command)) | 算术运算 |
2.2.3 数组
- 不要求连续,即一部分可以不被赋值,默认为NULL值
- 无类型限制,即可以存不同类型的值
声明:declare -a array
初始化:array=( 0 1 2 ) | ( [0]=0 [1]=1 [2]=2 )
赋值:array[0]=1 array[100]=2 array[200]=abc
使用:echo ${array[0]}
例子:
#!/bin/bash
area=( 1 2 3 [11]=11 [13]=13 )
area[5]=`expr ${area[11]} + ${area[13]}`
echo "area[5] = area[11] + area[13]"
echo "area[5] = "
echo ${area[5]}
#输出
#area[5] = area[11] + area[13]
#area[5] =
#24
2.3 内部变量
2.3.1 介绍
-
在执行 Shell 脚本的时候,可以传入参数,如当前有个脚本叫
test
,执行sh test arg1 arg2 arg3
,那么在test
中,$0
代表脚本文件名,$1
为第一个参数:arg1
,以此类推. -
使用
shift
可以将参数左移,此时$1
为arg2
,$2
为arg3
,$3
为空$#
为参数数量.特殊参数 含义 $#
传递到脚本的参数数量 $?
前一个命令执行情况, 0
成功,其他值失败$$
运行当前脚本的进程 id
$!
运行脚本最后一个命令 $@
传递给脚本或者函数的所有参数。 $0-9
位置参数,分别代表:当前执行的进程名,参数1,参数2…参数8,之后用shift,参数列表左移(即$1 <-- $2, $2 <-- $3…),获取更多参数 $*
传递给脚本或者函数的所有参数.若不设置IFS,则于 $@
等效;若设置IFS(参数连接符,默认为空格),参数间用它连接在一起。
2.3.2 参数条件置换
用法 | 意义 |
---|---|
var=${param:-default} | 如果param 已设置,则将param 赋给var ,否则default 赋给var |
var=${param:=default} | 如果param 已设置,则将param 赋给var ,否则default 赋给param 和var |
var=${param:?default} | 如果param 已设置,则将param 赋给var ,否则就显示default 并从shell 退出,如果省略了default ,则显示标准信息,指出错误. |
var=${param:+default} | param 已设置,则将default 赋给var ,否则不进行赋值而使用null 字符串 |
3. shell 输入输出
3.1 重定向
Linux 中默认输入输出分为 3 个文件:
- 标准输入
stdin
. 标准输入文件的编号是0
(牢记 Linux 万物皆文件,可以用文件表示设备),默认的设备是键盘,命令执行时从键盘读取数据. - 标准输出
stdout
. 标号为1
,默认的设备是显示器,命令的输出会被打印到屏幕上. - 标准错误
stderr
. 标号为2
,默认的设备是显示器,命令执行产生的错误信息会被发送到标准错误文件.
重定向的意思就是改变这三个文件实际指向,比如我们希望从某个文件中获取输入,那么就需要将标准输入指向这个文件. 重定向后命令依然从标准输入获得输入,此时标准输入指向这个文件,故命令能够从这个文件获取输入.
- 输入重定向
格式为命令 < 文件名
,比如sort < sp.txt
, 把sp.txt
文件中的内容作为sort
的输入. - 输出重定向
格式为命令 > 文件名
,比如cat /etc/passswd > ps.log
,cat
会输出/etc/passwd
中内容,但此时并不会输出到屏幕上,而是输出到ps.log
中. - 错误重定向
格式为命令 2> 文件名
,比如gcc -c test.c -o test.out 2> error.log
,如果gcc
编译时出现错误,则会把错误信息输出到error.log
中. 会覆盖原文件中内容,>>
则会将输出追加到原文件末尾. - 其他
- 在重定向错误时使用了错误文件的编号
2
,其实在输入输出的时候也可以显式写0
或1
,通常是省略. &
运算符,等价于:2>&1
. 表示将标准错误从重定向到标准输出指向的文件. 如1>/dev/null
,然后执行2>&1
,此时都指向空设备.
- 在重定向错误时使用了错误文件的编号
3.2 管道
管道作用是将多个命令串连起来,使一个命令的输出作为另一个命令的输入.
格式为命令1 | 命令2 | 命令3 ....| 命令n
.
比如 ls /etc | grep init
将会输出 /etc
目录下,文件名包含 init
的文件/目录. 如果不使用管道,命令就得拆成: ls /etc > tmp grep init < tmp rm tmp
或 ls /etc | grep init >> test cat test
.
其实管道是一种进程之间通信的手段,在之后的 Linux 系统编程的实验中我们还会经常遇到.
4. shell 控制流
4.1 条件表达式
两种形式:[ condition ]
| test condition
符号 | 含义(默认返回真的说明) |
---|---|
= | != | 比较字符串 |
-n str | str长度大于0 |
-z str | str长度为0 |
str | str不为空字符 |
-eq | -ge | -le | -gt | -ne | == | >= | <= | > | != |
-d file | file 是目录 |
-f file | file是普通文件 |
-r file | -w | -x | file是可读|可写|可执行文件 |
-s file | file长度大于0 |
! | 否 |
-a | -o(条件表达式内) | 与 | 或 |
&& | ||(条件表达式间) | 与 | 或 |
4.2 常用控制流
4.2.1 if
用法
if [ condition ]
then
...
elif [ condition ]
then
...
else
...
fi
if 例子1:equal.sh
#!/bin/bash
# 算术和字符串比较参数 1 和参数 2,若无对应参数,则默认值分别为 0 和 1
a=${1:-0}
b=${2:-1}
if [ $a -ne $b ]
then
echo "$a is not equal to $b"
else
echo "$a is equal to $b"
fi
echo "(arithmetic comparison)"
if [ $a != $b ]
then
echo "$a is not equal to $b"
else
echo "$a is euqal to $b"
fi
echo "(string comparison))"
if 例子2:range.sh
#!/bash/bin
# 输出参数 1 的大小范围
if [ $1 -le 10 ]
then
echo "a<=10"
elif [ $1 -le 20 ]
then
echo '10<a<=20'
else
echo "a>20"
fi
4.2.2 case
用法
case var in
expr1)
... ;;
1|3|5|7|9)
... ;;
*) # default
... ;;
esac
case 例子: yesorno.sh
#!/bin/bash
# 输入 y | Yes | YES 进入第一个入口,
# 输入以 n | N 开头的字符串则进入第二个入口,
# 其他输入进入第三个入口
echo "Enter yes or no?"
read answer
case $answer in
yes | y | Yes | YES)
echo "Good Morning!";;
[nN]*)
echo "Good Bye!";;
*)
echo "Sorry, invalid answer"
exit 1;;
esac
4.2.3 while/until
用法
while condition # 假则退出
do
command(s)
done
until condition # 真则退出
do
command(s)
done
while 例子:guess.sh
#!/bin/bash
# 猜密码
echo 'Enter password'
read pass
while [ $pass != 'secret' ]
do
echo "Sorry, try again"
read pass
done
echo "Success!"
4.2.4 for
用法
for arg in [list]
do
command(s)
done
for 例子:for.sh
#!/bin/bash
# 先打印列表,然后打印当前目录下的文件
for foo in bar fud 43
do
echo $foo
done
for file in `ls .`
do
echo "open file $file"
done
4.2.5 select
用法
selec var in arg1 arg2 arg3 ...
do
commands(var 为编号对应的参数)
done
select 例子: favorite.sh
#!/bin/bash
echo "输入编号选择: "
select subject in Math CS CH SP
do
echo "我最喜欢$subject"
break;
done
echo
5. shell 函数
5.1 函数简介
定义
[function] fun_name (){ // 也可以这样 function fun_name {}
...
[return ...] # 若没有return,则默认返回最后一条命令执行后的状态
[echo ...]
}
调用
fun_name arg1 arg2 ...
若使用return,则用$?接收返回值
若使用echo, 则当作表达似即可,如rlt=`fun_name arg1 ...`
5.2 函数例子
maxInThreeNums.sh
#!/bin/bash
# 该函数返回输入参数 1,参数2,参数3 中最大值
function max()
{
if [ $# -ne 3 ]
then
echo 'usage:max p1 p2 p3'
exit 1
fi
max=$1
if [ $max -lt $2 ]
then
max=$2
fi
if [ $max -lt $3 ]
then
max=$3
fi
return $max
# echo $max # 也可直接打印,无需使用下面的rlt
}
max $1 $2 $3
rlt=$?
echo "The max number of $1 $2 $3 is: $rlt"
exit