shell学习笔记
[TOC]
0. 前言
鉴于实习中经常使用 shell 语言,因此趁此机会好好学习一下“强大”的 shell 语言。
参考:
1. 简介
shell 直译为壳。在操作系统(Linux)中,被称为外壳,通常与 kernel 内核相对立。在没有图形界面的时代,shell 是用户与操作系统交互的接口。
shell 本身是一个程序。在操作系统实验课程中,老师曾让我们实现一个微型 shell ,代码见:https://github.com/99MyCql/OS_pratice。它包括 shell 的许多基本功能:命令提示符、运行命令/程序(如:cat、echo、ls等)、重定向输入输出、管道。完成实验时,成就感满满,几乎与真实的 shell 无疑。
但,当时的我可能忽略了 shell 的另一个“身份”——解释器。shell 解释器可运行 shell 脚本语言,它支持变量、条件判断、循环等等语法。这让 shell 具备了可编程性,而失去这一大功能的 shell 只能称为 mini shell 。
关于解释型语言的定义,以及与编译型语言的区别,就不在此赘述了。
shell 分为很多种,包括:Bourne Shell(sh)、Bourne Again shell(bash)、Z Shell(zsh)等等,它们的本质基本相同,本文将主要基于 Bash Shell 。
接触过 python 的hxd应该知道,python 既可以在解释器中一行一行地敲,也可以写在一个文件中再运行文件。
shell 也是同理,既可以运行在命令行,也可以写入脚本文件再执行。shell 脚本语言的代码文件以 .sh
结尾:
- 可以通过执行解释器执行:
bash test.sh
- 或者直接使用当前命令行的解释器执行:相对路径执行
./test.sh
或绝对路径/home/test/test.sh
(需为脚本添加可执行权限chmod +x test.sh
)
在 shell 脚本文件中,需要在第一行添加如下内容,指定解释器,如下指定 bash shell 为解释器:
1 |
|
shell 中注释为 #
。
2. 使用命令
在 shell 命令行中,可以输入cat、echo、grep等命令(这些命令本质是一个可执行文件),去执行对应的程序。
同样,在 shell 脚本中,也可以使用命令,包括内部命令和外部命令(在我看来 shell 语言中的命令就相当于其他语言中的库函数)。比如:
1 |
|
同时还包括:
- 管道
|
- 重定向
<
>
- 命令结束符
;
。使用:Command1 ; Command2
允许单行多个命令,第二个命令总是接着第一个命令执行,不管第一个命令执行成功或失败。 - 命令组合符
&&
||
。使用:Command1 && Command2
第一个命令运行成功,才继续运行第二个命令;Command1 || Command2
第一个命令运行失败,才继续运行第二个命令。
更多 Linux 命令可以参见我的另一篇笔记:linux命令学习笔记。
3. 变量
定义变量
1 | var=value |
变量名:由字母、数字和下划线字符组成;首字符必须是字母下划线,不能是数字。
变量值:没有数据类型的概念,都是字符串,如果值中包含空格,需使用引号包围。
注意:赋值符号附近不能有空格。
同时,可以将命令执行的结果赋给变量:
1 | var=`command` |
也可以将运算结果赋给变量:
1 | var=$((5 * 7)) |
使用变量
两种方式:
1 | $var |
花括号 {}
用于区分变量边界。比如:在如下代码中,不使用花括号,会把 varScript
当成变量。
1 | var="Java" |
如果变量值包含连续空格或制表符,使用变量时应用双引号 ""
包围起来,因为 Shell 会将多个空格合为一个:
1 | var="1 2 3" |
注意:当使用单引号 ''
将包围变量时,变量将不会解析,而是会被当成普通字符串:
1 | var="1 2 3" |
修改变量值
重新赋值即可:
1 | var="hello world" # hello world |
删除变量
1 | var="hello world" |
shell 中不存在的变量一律等于空字符串,所以即使unset命令删除了变量,还是可以读取这个变量(值为空字符串)。而且,被删除的变量可再次使用。
环境变量
用户创建的变量仅用于当前 Shell,子 Shell (在当前shell中运行的shell)默认读取不到父 Shell 定义的变量。
使用 export
命令可以设置变量为环境变量,使子 shell 可以读取该变量。
测试脚本 test.sh
如下:
1 |
|
运行:
1 | $ export test_export="export" # 设为环境变量,子 shell 可读 |
注意:子 Shell 如果修改环境变量,不会影响父 Shell 。
常用的环境变量有:
HOME
:用户的主目录。HOST
:当前主机的名称。PATH
:由冒号分开的目录列表,当输入可执行程序名后,会搜索这个目录列表。PWD
:当前工作目录。USER
:当前用户的用户名。LINENO
:返回它在脚本中的行号。FUNCNAME
:返回一个数组,内容是当前的函数调用堆栈。该数组的0号成员是当前调用的函数,1号成员是调用当前函数的函数。BASH_SOURCE
:返回一个数组,内容是当前的脚本调用堆栈。该数组的0号成员是当前执行的脚本,1号成员是调用当前脚本的脚本
只读变量
readonly
命令指示变量只读,不可修改。
1 | readonly var="hello" |
变量默认值
${var:-word}
如果变量 var 为空或已被删除(unset),那么返回 word,但不改变 var 的值。${var:=word}
如果变量 var 为空或已被删除(unset),那么返回 word,并将 var 的值设置为 word。${var:?message}
如果变量 var 为空或已被删除(unset),那么将消息 message 送到标准错误输出,并将脚本停止运行,可以用来检测变量 var 是否可以被正常赋值。${var:+word}
如果变量 var 被定义,那么返回 word,但不改变 var 的值。
特殊变量
$0
当前脚本的文件名。$n
传递给脚本或函数的参数,$1
表示第一个参数,$2
表示第二个参数。$#
传递给脚本或函数的参数个数。$*
传递给脚本或函数的所有参数。$@
传递给脚本或函数的所有参数。被双引号""
包含时,$*
会将所有参数作为一个整体,而$@
会分开。$?
上个命令的退出状态,或函数的返回值。$$
当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID。
4. 字符串
字符串是 shell 最基本的数据类型。
拼接字符串(推荐使用 {}
):
1 | str="world" |
获取字符串长度(变量使用必须要加 {}
):
1 | str="hello" |
提取字符串(offset
默认为0,length
默认到结尾):
1 | ${str:offset:length} |
字符转义:
- 经典的转义字符
\n
\t
转义; - 对于 shell 中的特殊字符,如
$
*
&
等,需要转义; - 使用单引号
''
时,转义字符都会被当成普通字符串
字符串匹配并删除:
${str#pattern}
从字符串首字符开始,删除最短匹配的部分,返回剩余字符串。pattern
支持*
、?
、[]
等通配符。${str##pattern}
从字符串首字符开始,删除最长匹配(贪婪匹配)的部分,返回剩余字符串。1
2
3
4
5
6str=/home/root/shell/study
str=${str#/*/} # root/shell/study
echo $str
echo ${str##/*/} # root/shell/study
str=/home/root/shell/study
echo ${str##/*/} # study${str%pattern}
从字符串尾字符开始,删除最短匹配的部分,返回剩余字符串。${str%%pattern}
从字符串尾字符开始,删除最长匹配的部分,返回剩余字符串。
更高级的匹配建议使用:grep
、awk
。
5. 数组
定义数组
1 | arr=(value0 value1 value2 value3) |
或
1 | arr=( |
或单独定义(可以不使用连续的下标,而且下标的范围没有限制):
1 | arr[0]=value0 |
追加
使用 +=
可追加元素:
1 | arr=(a b c d) |
使用数组
单个元素:
1 | value=${arr[i]} |
全部元素:
1 | ${arr[*]} |
注意:
- 默认
${arr} = ${arr[0]}
而非全部元素。 ${arr[@]}
"${arr[@]}"
${arr[*]}
"${arr[*]}"
有不同效果,详情见:读取所有成员,推荐使用"${arr[@]}"
多个元素:
1 | ${arr[@]:offset:length} |
获取数组长度:
1 | ${#arr[*]} |
获知数组哪个位置上有值,即获取数组中存在值的元素的索引(提取数组索引):
1 | unset arr |
6. 运算表达式
语法:使用 (( ))
包裹,或者使用 expr
命令。更推荐前一种。
获取表达式的结果:$(( ))
。
在表达式中可以使用变量,且**不需要加$
**。若变量为空,则当作 0 。
在表达式中,可以使用进制:默认十进制、0num
八进制、0xnum
十六进制、base#num
base进制
算术运算
+
加法-
减法*
乘法/
除法(整除)%
余数**
指数++
自增运算(前缀或后缀)--
自减运算(前缀或后缀)
1 | i=0 |
位运算
与 C 语言一致:
<<
左移>>
右移&
与|
或~
按位取反^
异或
1 | echo $((16>>2)) # 4 |
逻辑运算
与 C 语言一致:
<
小于>
大于<=
小于或相等>=
大于或相等==
相等!=
不相等&&
逻辑与||
逻辑或!
逻辑否expr1 ? expr2 : expr3
三元条件运算
如果逻辑表达式为真,返回1,否则返回0:
1 | echo $((3 > 2)) # 1 |
赋值运算
支持直接赋值 =
,也支持 +=
*=
|=
等等。
1 | i=1 |
7. 条件判断 if
1 | if commands |
if
后面所接的判断条件是一个命令,命令返回成功(0)则为真,返回失败(非0)则为假。
test
if
判断条件通常使用 test
命令,它是一个用于判断的命令,它有三种形式:
1 | # 写法一 |
注意:
- 中括号
[ ]
与表达式之间必须包含空格 - 第二种形式与第三种形式,在某些场景(比如逻辑判断)有所不同,详情参考:https://www.zsythink.net/archives/2252
由于 test
是一个命令,它支持很多选项:
1) 文件判断
[ -a $file ]
:如果 file 存在,则为true。[ -b $file ]
:如果 file 存在并且是一个块(设备)文件,则为true。[ -c $file ]
:如果 file 存在并且是一个字符(设备)文件,则为true。[ -d $file ]
:如果 file 存在并且是一个目录,则为true。[ -e $file ]
:如果 file 存在,则为true。[ -f $file ]
:如果 file 存在并且是一个普通文件,则为true。
更多见:条件判断
2) 字符串判断
[ $str ]
:如果str不为空(长度大于0),则判断为真。[ -n $str ]
:如果字符串str的长度大于零,则判断为真。[ -z $str ]
:如果字符串str的长度为零,则判断为真。[ $str1 = $str2 ]
:如果str1和str2相同,则判断为真。[ $str1 == $str2 ]
:等同于[ $str1 = $str2 ]。[ $str1 != $str2 ]
:如果str1和str2不相同,则判断为真。[ $str1 '>' $str2 ]
:如果按照字典顺序str1排列在str2之后,则判断为真。[ $str1 '<' $str2 ]
:如果按照字典顺序str1排列在str2之前,则判断为真。
注意:test命令内部的>
和<
,必须用引号括起来(或者是用反斜杠转义),否则它们会被 shell 解释为重定向操作符。
3) 整数判断
由于 >
<
会被误解为重定向操作法,所以有专门的整数判断指令。
[ $int1 -eq $int2 ]
:如果int1等于int2,则为true。[ $int1 -ne $int2 ]
:如果int1不等于int2,则为true。[ $int1 -le $int2 ]
:如果int1小于或等于int2,则为true。[ $int1 -lt $int2 ]
:如果int1小于int2,则为true。[ $int1 -ge $int2 ]
:如果int1大于或等于int2,则为true。[ $int1 -gt $int2 ]
:如果int1大于int2,则为true。
4) 逻辑判断
[[ $expr1 && $expr1 ]]
/[ $expr1 ] && [ $expr1 ]
[[ $expr1 || $expr1 ]]
/[ $expr1 ] || [ $expr1 ]
[ ! $expr1 ]
/[ ! \( $expr1 && $expr2 \) ]
注意:test命令内部使用(
和)
必须使用引号或转义。
运算表达式
if
判断条件也可以使用运算表达式 (( ))
。
但注意:运算表达式返回非0 ((1))
表示真,返回0 ((0))
表示假。
1 | echo $((2 > 1)) # 1 |
普通命令
if
判断条件可以直接使用命令,命令返回成功(0)则为真,返回失败(非0)则为假。
当然,也可以使用管道、重定向、命令结束符;
、命令组合符&&
||
等。
比如:
1 | if mkdir temp && cd temp; then |
8. case
1 | case expression in |
pattern
支持基本的模式匹配,比如:
1 | echo -n "输入一个字母或数字 > " |
9. 循环
while
1 | while commands; do |
判断条件与 if
一样。
until
1 | until commands; do |
for
遍历列表每一项:
1 | for variable in ${arr[@]}; do |
或:
1 | for (( expr1; expr2; expr3 )); do |
比如:
1 | for ((i=0; i<10; i++)); do |
continue
提前终止本轮循环,进行下一轮循环。
10. 函数
定义:
1 | # 第一种 |
调用:
1 | func # 直接调用无参数 |
参数
$1~$9
:函数的第1个到第9个的参数。$0
:函数所在的脚本名。$#
:函数的参数总数。$@
:函数的全部参数,参数之间使用空格分隔。$*
:函数的全部参数,参数之间使用变量$IFS
值的第一个字符分隔,默认为空格,但是可以自定义。
return
函数返回,可指定返回值,调用者通过 $?
获取。
local 局部变量
shell 中定义变量属于全局变量,在函数中声明局部变量需使用 local
,比如:
1 | func1() { |
其它
主要介绍在脚本中使用较多,而在命令行中使用较少的命令。
set
命令行下不带任何参数,直接运行 set
,会显示所有的环境变量和 Shell 函数。
常用选项:
set -u
遇到不存在的变量则报错(默认会跳过)set -x
在运行命令前,先输出该命令,常用于调试。set -x
开启,set +x
关闭。set -e
遇到错误则终止执行(默认命令执行出错会忽略)。set -e
有一个例外情况,就是不适用于管道命令(多个子命令通过管道符组合,Bash 会把最后一个子命令的返回值,作为整个命令的返回值)。set -o pipefail
用来解决这种情况,只要一个子命令失败,整个管道命令就失败,脚本就会终止执行。
使用示例:
1 | # set -x # 调试时再开启 |
注意:
- 使用
set -e
后,如果调用函数,函数返回了非零值,程序也会退出!
read
1 | read [-options] [var...] |
输入由回车结束,用户输入将被保存到变量 var
,多个输入项通过空格区分。
若未提供变量名,环境变量 REPLY
会包含用户输入的一整行数据。
若提供的输入项少于变量数目,则剩余变量为空。
常用选项:
p
指定提示信息。
1 | read -p "Enter your input:" |
a
把用户的输入赋值给一个数组,从零号位置开始。
1 | read -a arr |
n
指定只读取若干个字符作为变量值,而不是整行读取。
1 | read -n 3 var |
read 还可用于读文件:
1 | filename='xxx' |
exit
用于退出当前执行的 Shell ,并返回一个值,返回 0 代表成功,返回 非0 代表失败。
source
用于执行一个脚本文件,但不同于直接执行(会新建子 shell ),source
会在当前 shell 执行。
类似于加载外部库。