本文转载自 Shell 脚本深入教程:快速入门
Shell 脚本基础入门
bash注释
Bash 只支持单行注释,使用#
开头的都被当作注释语句:
|
|
通过 Bash 的一些特性,可以取巧实现多行注释:
|
|
但是,别闲的蛋疼去用取巧的多行注释,安心用#
来注释。
bash基本数据类型
Bash 中基本数据类型只有字符串类型,连数值类型都没有 (declare -i
可强制声明数值类型)。
比如:
|
|
bash字符串串联
Bash 中字符串的串联操作,直接将两段数据连接在一起即可,不需要任何操作符。
例如:
|
|
命令基本知识
变量赋值和引用变量
等号左右不能有空格
|
|
Shell 中可以引用未定义的变量:
|
|
可以定义空变量:
|
|
变量替换
变量替换是指在命令开始执行前,Shell 会先将变量的值替换到引用变量的位置处。
例如:
|
|
在 echo 命令开始执行前,Shell 会取得变量 a 的值 hello,并将它替换到命令行的 $a 处。于是,在 echo 命令开始执行时,命令行已经变成:
|
|
除了变量替换,Shell 还会做其它替换:
- 命令替换
- 进程替换
- 算术运算替换
- 大括号扩展
- 波浪号扩展
- 路径扩展
这些扩展和替换,都是 Shell 在调用命令之前就完成的,这和其它语言解析代码的方式不一样。
后面会详细解释 Shell 是如何做命令行解析的,如果不掌握命令行解析,当遇到命令行语法错误后很可能会花掉大量无谓的时间去调试命令。而掌握命令行解析后, 就会对命令生命周期了如执掌,不敢说一次就能写对所有命令行,但能节省大量调试时间,对写命令行和写脚本的能力也会上升一个层次。
命令替换
使用反引号或 $()
可以执行命令替换。
|
|
命令替换是指先执行 cmd,将 cmd 的输出结果替换到 $()
或反引号位置处。
例如:
|
|
在 echo 命令执行前,会先执行 id 命令,id 命令的执行结果:
|
|
所以会将结果 uid=0(root) gid=0(root) groups=0(root)
替换 $(id root)
。于是,echo 命令开始执行时,命令行已经变成了:
|
|
算术运算
$[]
或 $(())
或 let 命令可以做算术运算。
let 是单独的命令,不能写在其它命令行中。
|
|
$[]
和 $(())
可以写在命令行内部,Shell 在解析命令行的时候,会对它们做算术运算,然后将运算结果替换到命令行中。
|
|
因为变量替换先于算术替换,所以,使用变量名或引用变量的方式都可以:
|
|
退出状态码
每个命令执行后都会有对应的进程退出状态码,用来表示该进程是否是正常退出。
所以,在命令行中,在 Shell 脚本中,经常会使用特殊变量 $?
判断最近一个前台命令是否正常退出。
通常情况下,如果 $?
的值:
- 为 0,表示进程成功执行,即正常退出
- 非 0,表示进程未成功执行,即非正常退出
- 但非 0 退出状态码并不一定表示错误,也可能是正常逻辑的退出
另外,在 Shell 脚本中,所有条件判断 (比如 if 语句、while 语句) 都以 0 退出状态码表示 True,以非 0 退出状态码为 False。
exit命令
exit 命令可用于退出当前 Shell 进程,比如退出当前 Shell 终端、退出 Shell 脚本,等等。
|
|
exit 可指定退出状态码 N,如果省略 N,则默认退出状态码为 0,即表示正确退出。
后台执行命令 &
在命令的结尾使用 &
符号,可以将这个命令放入后台执行。
命令放入后台后,会立即回到 Shell 进程,Shell 进程会立即执行下一条命令 (如果有) 或退出。
使用 $!
可以获取最近一个后台进程的 PID。
|
|
使用 wait
命令可以等待后台进程 (当前 Shell 进程的子进程) 完成:
|
|
不给定任何参数时,会等待所有子进程 (即所有后台进程) 完成。
|
|
多命令组合
Shell 中有多种组合多个命令的方式。
1.cmd1 退出后,执行 cmd2
|
|
2.cmd1 正确退出 (退出状态码为 0) 后,执行 cmd2
|
|
3.cmd1 不正确退出后,执行 cmd2
|
|
4.逻辑结合:&&
和 ||
可以随意结合
|
|
5.将多个命令分组:小括号或大括号可以组合多个命令
|
|
基本重定向
软件设计认为,程序应该有一个数据来源、数据出口和报告错误的地方。在 Linux 系统中,每个程序默认都会打开三个文件描述符 (file descriptor,fd):
- fd=0:标准输入,表示程序默认从哪里读取数据
- fd=1:标准输出,表示程序默认将数据输出到哪里
- fd=2:标准错误,表示程序默认将错误信息输出到哪里
文件描述符,说白了就是系统为了跟踪打开的文件而分配给它的一个数字,这个数字和文件有对应关系:从文件描述符读取数据,即表示从对应的文件中读取数据, 向文件描述符写数据,即表示向对应文件中写入数据。
Linux 中万物皆文件,文件描述符也是文件。默认:
- fd=0 的标准输入是 /dev/stdin 文件
- fd=1 的标准输出是 /dev/stdout 文件
- fd=2 的标准错误是 /dev/stderr 文件
这些文件默认又是各个终端的软链接文件:
|
|
所以,默认情况下读写数据都是终端,例如:
|
|
改变文件描述符对应的目标,可以改变数据的流向。比如标准输出 fd=1 默认流向是终端设备,若将其改为 /tmp/a.log,便能让数据写入 /tmp/a.log 文件中而不再是终端设备中。
在 Shell 中,这种改变文件描述符目标的行为称为重定向,即重新确定数据的流向。
其实,文件描述符有很多类操作,包括 fd 的重定向、fd 的分配 (open,即打开文件)、fd 复制 (duplicate)、fd 的移动 (move)、fd 的关闭 (close)。现在只介绍基础重定向操作。
Shell 中,基础重定向操作有以下几种方式:
- [n]>file:覆盖式输出重定向,输出到 fd=n 的数据改变流向输出到 file 文件中,file 不存在则创建,file 存在则先清空再写入数据
- 省略 n 时 >file,等价于 1>file,即标准输出覆盖重定向到 file 文件中
- [n]>>file:追加式输出重定向,输出到 fd=n 的数据改变流向输出到 file 文件的尾部,file 不存在则创建,file 存在则直接追加在文件尾部
- 省略 n 时 »file,等价于 1»file,即标准输出追加重定向到 file 文件中
- [n]<file:输入重定向,以读取模式打开 file 文件并分配 fd=n,file 不存在则报错
- 省略 n 时 <file,等价于 0<file,即直接从 file 中读数据
- 通常程序都只从 fd=0 中读数据,所以当 n 不等于 0 时,需要多做一步操作 3<file <&3,看不懂先跳过
- &>file:这是特殊的重定向方式,表示将标准错误和标准输出都重定向到 file 文件中,等价于 >file 2>&1
- &>>file:这是特殊的重定向方式,表示将标准错误和标准输出都追加到 file 文件中,等价于 »file 2>&1
另外,经常用于输出的一个特殊目标文件是 /dev/null,它是空设备,可以直接丢掉所有写入它的数据。
|
|
一个经常用的技巧是清空文件的方式:
|
|
区分 cat <file 和 cat file
cat 是一个命令,这个命令的源代码中写了一些代码用来处理选项和参数。
|
|
cat 命令开始执行后,会识别 -n
选项,该选项会让 cat 输出时同时输出行号,cat 同时还会识别 /etc/fstab
参数,cat 会读取参数指定的文件然后输出。
如果没有指定 cat 的文件参数,则 cat 默认会从标准输入中读取数据。默认的标准输入是终端,所以在没有改变标准输入的流向时,会从终端读取数据, 也就是用户输入什么字符,就读取什么字符,然后输出什么字符:
|
|
但用户可以改变标准输入的来源。比如:
|
|
表示将标准输入来源改为 /etc/fstab 文件,于是 cat 会从 /etc/fstab 中读取数据。
另外,约定俗成的,会使用一个 -
来表示标准输入或标准输出。
注:这并非是一贯正确的,只是约定俗成的大多数程序的代码中都定义了 - 相关的代码处理。可参考相关命令的 man 手册。如 man cat 中有一行:
|
|
here doc
输入重定向是 <
,除此之外还有 <<
、<<<
。
<<
符号表示 here doc。也就是说,它后面跟的是一篇文档,就像一个文件一样,只不过这个文件的内容是临时定义在 <<
符号后的。here doc 常用于指定多行数据输入。
既然是文档,就有文档起始符号表示文档从此开始和文档终止符号表示文档到此结束。起始符和终止符中间的内容全部是文档内容。文档内容会作为标准输入的数据被读取。
文档的起始符和终止符可以随意定义,但两者前后必须一样。常见的符号是:
- EOF:end of file
- EOL:end of line
- EOB:end of block
例如:
|
|
另外,如果将起始符用引号包围,则不会进行变量替换、命令替换、算术替换等。如果不用引号包围起始符,则会进行替换。
|
|
输出结果:
|
|
here string
<<<
表示 here string。也就是说该符号后面是一个字符串,这个字符串会作为标准输入的内容。
|
|
使用单引号包围 here string 时,不会进行变量替换、命令替换等,使用双引号包围时会进行替换。
|
|
here string 常可以替代管道前的 echo 命令 echo xxx|
。例如:
|
|
管道
管道的用法:
|
|
每个竖线代表一个管道。上面命令行表示 cmd1 的标准输出会放进管道,cmd2 会从管道中读取进行处理,cmd2 的标准输出会放入另一个管道,cmd3 会从这个管道中读取数据进行处理。后面还可以接任意数量的管道。
Shell 管道是 Shell 中最值得称赞的功能之一,它以非常简洁的形式实现了管道的进程间通信方式,我个人认为 Shell 处理文本数据的半壁江山都来自于竖线形式的管道。像其它编程语言,打开管道后还要区分哪个进程写管道、哪个进程读管道,为了安全,每个进程还要关闭不用的读端或写端,总之就是麻烦,而 Shell 的管道非常简洁,竖线左边的就是写管道的,竖线右边的就是读管道的。
例如:
|
|
ps 命令产生的数据 (标准输出) 会写进管道,只要管道内一有数据,grep 命令就从中读取数据进行处理。
那下面的命令中,grep 从哪读数据呢?
|
|
那想要让 grep 既从 /etc/fstab 读取数据,也从管道中读取数据呢?
|
|
tee命令
tee 命令可将一份标准输入原样拷贝到标准输出和 0 或多个文件中。换句话说,tee 的作用是数据多重定向。
|
|
如图:
例如:
|
|
进程替换
Bash 还支持进程替换 (注:有些 Shell 不支持进程替换)。
进程替换的语法:
|
|
进程替换和命令替换类似,都是让 cmd 命令先执行,因为它们都是在 Shell 解析命令行的阶段执行的。
进程替换先让 cmd 放入后台异步执行,并且不会等待 cmd 执行完。
其实,每个进程替换都是一个虚拟文件,只不过这个文件的内容是由 cmd 命令产生的 (<(cmd))
或被 cmd 命令读取的 (>(cmd))
。
|
|
既然进程替换是文件,那么它就可以像文件一样被操作。比如被读取、被当作标准输入重定向的数据源等等:
|
|
条件测试语句
test 命令或功能等价的 Bash 内置命令 [ ]
可以做条件测试,如果测试的结果为 True,则退出状态码为 0。
此外,还可以使用 [[]]
来做条件测试,甚至 let、$[]、$(())
也可以做条件测试,但这里暂不介绍。
这些条件测试常用在 if、while 语句中,也常用在 cmd1 && cmd2 || cmd3
格式的命令行中。
用法示例:
|
|
[]
中的条件测试表达式需要和开闭中括号使用空格隔开,否则语法解析错误。
无测试内容
|
|
没有任何测试内容时,直接返回 false。
true和false命令
true 命令直接返回 true,即退出状态码为 0。
false 命令直接返回 false,即退出状态码非 0。
|
|
文件类测试
条件表达式 | 含义 |
---|---|
-e file | 文件是否存在 (exist) |
-f file | 文件是否存在且为普通文件 (file) |
-d file | 文件是否存在且为目录 (directory) |
-b file | 文件是否存在且为块设备 block device |
-c file | 文件是否存在且为字符设备 character device |
-S file | 文件是否存在且为套接字文件 Socket |
-p file | 文件是否存在且为命名管道文件 FIFO (pipe) |
-L file | 文件是否存在且是一个链接文件 (Link) |
文件属性类测试
条件表达式 | 含义 |
---|---|
-r file | 文件是否存在且当前用户可读 |
-w file | 文件是否存在且当前用户可写 |
-x file | 文件是否存在且当前用户可执行 |
-s file | 文件是否存在且大小大于 0 字节,即检测文件是否非空文件 |
-N file | 文件是否存在,且自上次 read 后是否被 modify |
两文件之间的比较
条件表达式 | 含义 |
---|---|
file1 -nt file2 | (newer than) 判断 file1 是否比 file2 新 |
file1 -ot file2 | (older than) 判断 file1 是否比 file2 旧 |
file1 -ef file2 | (equal file) 判断 file1 与 file2 是否为同一文件 |
数值大小比较
条件表达式 | 含义 |
---|---|
int1 -eq int2 | 两数值相等 (equal) |
int1 -ne int2 | 两数值不相等 (not equal) |
int1 -gt int2 | n1 大于 n2 (greater than) |
int1 -lt int2 | n1 小于 n2 (less than) |
int1 -ge int2 | n1 大于等于 n2 (greater than or equal) |
int1 -le int2 | n1 小于等于 n2 (less than or equal) |
字符串比较
条件表达式 | 含义 |
---|---|
-z str | (zero) 判定字符串是否为空?str 为空串,则 true |
str -n str | 判定字符串是否非空?str 为串,则 false。注:-n 可省略 |
str1 = str2/str1 == str2 | str1 和 str2 是否相同,相同则返回 true。”==” 和”=” 等价 |
str1 != str2 | str1 是否不等于 str2,若不等,则返回 true |
str1 > str2 | str1 字母顺序是否大于 str2,若大于则返回 true |
str1 < str2 | str1 字母顺序是否小于 str2,若小于则返回 true |
逻辑运算符
条件表达式 | 含义 |
---|---|
-a 或 && | (and) 两表达式同时为 true 时才为 true。“-a” 只能在 test 或 [] 中使用,&& 只能在 [[]] 中使用 |
-o 或 || | (or) 两表达式任何一个 true 则为 true。“-o” 只能在 test 或 [] 中使用,||只能在 [[]] 中使用 |
! | 对表达式取反 |
() | 改变表达式的优先级,为了防止被 shell 解析,应加上反斜线转义 ( ) |
if语句
|
|
test-commands 既可以是 test 测试或 []、[[]]
测试,也可以是任何其它命令,test-commands 用于条件测试,它只判断命令的退出状态码是否为 0,为 0 则为 true。
例如:
|
|
case语句
case 常用于确定的分支判断。比如:
|
|
case 用法基本要求:
- 除最后一个分支外,每个分支都以 ;; 结尾,否则出现分支穿透 (所以 ;; 不是必须的)
- 分支条件可以使用通配符号
- 分支条件中可使用竖线隔开多个条件,表示只要匹配其中之一就执行该分支
- 最后一般会定义一个能匹配其它任意条件的默认分支,即 *)
下面是为 ping 命令自定义设计选项的脚本:
|
|
执行:
|
|
for循环
有两种 for 循环结构:
|
|
成员测试类的 for 循环中,in 关键字后是使用空格分隔的一个或多个元素,for 循环时,每次从 in 关键字后面取一个元素并赋值给 i 变量。
例如:
|
|
C 语言型的 for 语法中,expr1 是初始化语句,expr2 是循环终点条件判断语句,expr3 是每轮循环后执行的语句,一般用来更改条件判断相关的变量。
|
|
while循环
|
|
while 循环,开始时会测试 test_cmd_list
,如果测试的退出状态码为 0,则执行一次循环体语句 cmd_list
,然后再测试 test_cmd_list
,一直循环,直到测试退出状态码非 0,循环退出。
例如:
|
|
还有 until 循环语句,但在 Shell 中用的很少。
while 循环经常会和 read 命令一起使用,read 是 Bash 的内置命令,可用来读取文件,通常会按行读取:每次读一行。
例如:
|
|
上面的命令行中,首先 cat 进程和 while 结构开始运行,while 结构中的 read 命令从标准输入中读取,也就是从管道中读取数据,每次读取一行,因为管道中最初没有数据,所以 read 命令被阻塞处于数据等待状态。当 cat 命令读完文件所有数据后,将数据放入到管道中,于是 read 命令从管道中每次读取一行并将所读行赋值给变量 line,然后执行循环体,然后继续循环,直到 read 读完所有数据,循环退出。
但注意,管道两边的命令默认是在子 Shell 中执行的,所以其设置的变量在命令执行完成后就消失。换句话说,在父 Shell 中无法访问这些变量。比如上面的 num 变量是在管道的 while 结构中设置的,除了在 while 中能访问该变量,其它任何地方都无法访问它。
如果想要访问 while 中赋值的变量,就不能使用管道。如果是直接从文件读取,可使用输入重定向,如果是读取命令产生的数据,可使用进程替换。
|
|
shell函数
Shell 函数可以当作命令一样执行,它是一个或多个命令的组合结构体。通常,可以为每个功能定义一个函数,该函数中包含实现这个功能相关的所有命令和逻辑。
因为可以组合多个命令,并且定义之后就可以直接在当前 Shell 中调用,所以函数具有一次定义多次调用且代码复用的功能。
Shell 函数的定义风格有下面几种:
|
|
函数定义后,可以直接使用函数名来调用函数,同时还可以向函数传递零个或多个参数。
|
|
在函数中,那些位置变量将有特殊的意义:
$1
、$2
、$3
…:传递给函数的第一个参数保存在$1
中,第二个参数保存在$2
中…$@
和$*
:保存了所有参数,各参数使用空格分隔- 不用双引号包围时,两者没区别
- 双引号包围时,
$@
的各个元素都被双引号包围,$*
的所有元素一次性被双引号包围
例如,定义一个函数专门用来设置和代理相关的变量:
|
|
上面在函数定义的代码中使用了 local
,它可以用在函数内部表示定义一个局部变量,局部变量在函数执行完毕后就消失,不会影响函数外部的环境。
另外,函数中可以使用 return 语句来定义函数的返回值,每当执行到函数内的 return 时,函数就会终止执行,直接退出函数。在 Shell 中,函数的返回值其实就是退出状态码。
|
|
如果不指定 N,则默认退出状态码为 0。
例如:
|
|