书写优雅的shell脚本(一)- if语句
使用 unix/linux 的程序人员几乎都写过 shell 脚本,但这其中很多人都是为了完成功能而在网上找代码段,这样写出来的 shell 脚本在功能方面当然是没有什么问题,但是这样的方式不能写出优雅的 shell 脚本。 从今天开始,就将自己平时在书写 shell 脚本过程中的经历做一总结,力图形成一个系列 --- “书写优雅的 shell 脚本”。
在此,对“优雅”一词的定义有 4 点:
- 健壮
- 结构清晰
- 性能好
- 力求简单
好了,废话不多说,开始今天的主题:if 语句
1.1 if 判断式
格式一:
1 2 3 |
|
格式二:
1 2 3 4 5 |
|
格式三:
1 2 3 4 5 6 7 8 |
|
1.2 if 中的二元比较
1.2.1 整数比较
- -eq 等于,如: if [ $a -eq $b ]
- -ne 不等于,如: if [ $a -ne $b ]
- -gt 大于,如: if [ $a -gt $b ]
- -ge 大于等于,如: if [ $a -ge $b ]
- -lt 小于,如: if [ $a -lt $b ]
- -le 小于等于,如: if [ $a -le $b ]
- > 大于,如: if [ $a > $b ]
- >= 大于等于,如: if [ $a >= $b ]
注:以上其实不是健壮的代码,上面这些代码在有些情况下会存现错误提示,而真正健壮的是使用双括号来表示,即 if [[ $a -eq $b ]] 。
这是为何?做个测试如下:
1 2 3 |
|
而改为
1 |
|
将不再报错,这也是我们所期望的。
(注:原文中改为 if [[ a > $b ]]; then echo "true"; if ,这种改法是错误的,将永远输出 true 。 )
究其原因,是因为如果变量 a 值为空(由于 shell 是弱类型语言,对变量赋值都是当字符串对待),那么就成了 [ -gt 3 ] ,显然 [ 和 $b 不相等,并且缺少了 [ 符号,所以报了这样的错误。当然不总是出错,如果变量 a 值不为空,程序就正常了,所以这样的错误还是很隐蔽的。
1.2.2 字符串比较
- 等于,如:if [ $a = $b ] 或 if [ $a == $b ] ,与 = 等价
- 不等于,如:if [ $a != $b ]
- 大于,在 ASCII 字母顺序下。如:if [ $a \> $b ]
- 小于,在 ASCII 字母顺序下。如:if [ $a \< $b ]
注意:要使用转义符“\”。
1.2.3 文件比较
- [ 文件1 -nt 文件2 ] 为真,如果文件 1 被 changed more recently than 文件 2 ,或者如果文件 1 存在,而文件 2 不存在。
- [ 文件1 -ot 文件2 ] 为真,如果文件 1 比文件 2 旧, 或者文件 2 存在而文件 1 不存在。
- [ 文件1 -ef 文件2 ] 为真,如果文件 1 和 文件 2 均 refer to the same device and inode numbers。
1.2.4 表达式比较
- [ 表达式1 -a 表达式2 ] 如果表达式 1 和表达式 2 同时为真则为真 。
- [ 表达式1 -o 表达式2 ] 如果表达式 1 或者表达式 2 其中之一为真则为真。
1.3 if 中的一元比较
- [ -a 文件 ] 如果文件存在,则为真。
- [ -b 文件 ] 如果文件存在,并且是一个块文件,则为真。
- [ -c 文件 ] 如果文件存在,并且是一个字符文件,则为真。
- [ -d 文件 ] 如果文件存在,并且是一个目录,则为真。
- [ -e 文件 ] 如果文件存在,则为真。
- [ -f 文件 ] 如果文件存在,并且是一个普通文件,则为真。
- [ -g 文件 ] 如果文件存在,并且已经设置了 SGID 位,则为真。
- [ -h 文件 ] 如果文件存在,并且是一个符号连接,则为真。
- [ -k 文件 ] 如果文件存在,并且其 sticky 位已经设置,则为真。
- [ -p 文件 ] 如果文件存在,并且是一个命名管道,则为真。
- [ -r 文件 ] 如果文件存在,并且是可读的,则为真。
- [ -s 文件 ] 如果文件存在,并且比零字节大,则为真。
- [ -t FD ] 如果文件存在,并且文件描述符已经打开,且指向一个终端,则为真。
- [ -u 文件 ] 如果文件存在,并且已经设置了其 SUID(set user ID) 位,则为真。
- [ -w 文件 ] 如果文件存在,并且文件是可写的,则为真。
- [ -x 文件 ] 如果文件存在,并且文件是可执行的,则为真。
- [ -O 文件 ] 如果文件存在,并且属于有效用户 ID ,则为真。
- [ -G 文件 ] 如果文件存在,并且属于有效组 ID ,则为真。
- [ -L 文件 ] 如果文件存在,并且是一个符号连接,则为真。
- [ -N 文件 ] 如果文件存在,并且如果 has been modified since it was last read ,则为真。
- [ -S 文件 ] 如果文件存在,并且是一个 socket ,则为真。
- [ -o 选项名 ] 如果 shell 选项 "选项名" 开启,则为真。
- [ -z STRING ] 如果 "STRING" 的长度是零,则为真。
- [ -n STRING ] 或者 [ STRING ] 如果 "STRING" 的长度是非零值,则为真。
- [ ! EXPR ] 如果 EXPR 为假,则为真。
- [ ( EXPR ) ] 返回 EXPR 的值。 这样可以用来忽略正常的操作符优先级。
=== 我是YOYO的分隔线 ===
书写优雅的shell脚本(二)- `dirname $0`
在命令行状态下单纯执行 $ cd `dirname $0` 是毫无意义的。因为其 cd 到的路径是 cd 命令执行的当前路径的。
这个命令写在脚本文件里才有作用,其返回该脚本文件所在目录的路径,并可以根据这个目录来定位所要运行程序的相对位置(绝对位置除外)。
在 /home/admin/test/ 下新建 test.sh 内容如下:
1 2 3 4 |
|
然后返回到 /home/admin/ 执行
1 |
|
运行结果:
1 |
|
这样就可以知道一些和脚本一起部署的文件的位置了,只要知道相对位置就可以根据这个目录来定位,而可以不用关心绝对位置。这样脚本的可移植性就提高了,扔到任何一台服务器,(如果是部署脚本)都可以执行。
=== 我是乐观的分隔线 ===
书写优雅的shell脚本(三) - shell中exec解析
参考:《linux命令、编辑器与shell编程》、《unix环境高级编程》
exec 和 source 都属于 bash 内部命令(builtins commands),在 bash 下输入 man exec 或 man source 可以查看所有的内部命令信息。
bash shell 的命令分为两类:外部命令和内部命令。外部命令是通过系统调用或独立的程序实现的,如 sed、awk 等等。内部命令是由特殊的文件格式(.def)所实现,如 cd、history、exec 等等。
在说明 exec 和 source 的区别之前,先说明一下 fork 的概念。 fork 是 linux 的系统调用,用来创建子进程(child process)。子进程是父进程(parent process)的一个副本,从父进程那里获得一定的资源分配以及继承父进程的环境。子进程与父进程唯一不同的地方在于 pid(process id)。环境变量(传给子进程的变量,遗传性是本地变量和环境变量的根本区别)只能单向从父进程传给子进程。不管子进程的环境变量如何变化,都不会影响父进程的环境变量。
有两种方法执行 shell script 。一种是新产生一个 shell,然后执行相应的 shell script ;另一种是在当前 shell 下执行,不再启用其他 shell 。 新产生一个 shell 然后再执行 script 的方法是在 script 文件开头加入以下语句
1 |
|
一般的 script 文件(.sh)即是这种用法。这种方法先启用新的 sub-shell(新的子进程),然后在其下执行命令。 另外一种方法就是上面说过的 source 命令,不再产生新的 shell,而在当前 shell 下执行一切命令。
source 命令即点(.)命令。 在 bash 下输入 man source,找到 source 命令解释处,可以看到解释 ”Read and execute commands from filename in the current shell environment and …”。从中可以知道,source 命令是在当前进程中执行参数文件中的各个命令,而不是另起子进程(或 sub-shell)。
在 bash 下输入 man exec ,找到 exec 命令解释处,可以看到有”No new process is created.”这样的解释,这就是说 exec 命令不产生新的子进程。
exec 与 source 的区别是什么呢?
系统调用 exec 是以新的进程去代替原来的进程,但进程的 PID 保持不变。因此可以这样认为,exec 系统调用并没有创建新的进程,只是替换了原来进程上下文的内容,即原进程的代码段,数据段,堆栈段被新的进程所代替。
一个进程主要包括以下几个方面的内容:
- 一个可以执行的程序
- 与进程相关联的全部数据(包括变量,内存,缓冲区)
- 程序上下文(程序计数器 PC ,保存程序执行的位置)
执行 exec 系统调用,一般都是这样,用 fork() 函数新建立一个进程,然后让进程去执行 exec 调用。我们知道,在 fork() 建立新进程之后,父进各与子进程共享代码段,但数据空间是分开的,但父进程会把自己数据空间的内容 copy 到子进程中去,还有上下文也会 copy 到子进程中去。而为了提高效率,采用一种写时 copy 的策略,即创建子进程的时候,并不 copy 父进程的地址空间,父子进程拥有共同的地址空间,只有当子进程需要写入数据时(如向缓冲区写入数据),这时候会复制地址空间,复制缓冲区到子进程中去。从而父子进程拥有独立的地址空间。而对于 fork() 之后执行 exec 后,这种策略能够很好的提高效率,如果一开始就 copy ,那么 exec 之后,子进程的数据会被放弃,被新的进程所代替。
exec 与 system 的区别是什么呢?
- exec 是直接用新的进程去代替原来的程序运行,运行完毕之后不回到原先的程序中去。
- system 是调用 shell 执行你的命令。
system = fork + exec + waitpid
执行完毕之后,回到原先的程序中去。继续执行下面的部分。
总之,如果你用 exec 调用,首先应该 fork 一个新的进程,然后 exec 。而 system 不需要你 fork 新进程,已经封装好了。
=== 我是九喇嘛的分隔线 ===
书写优雅的shell脚本(四) - kill命令的合理使用
...
kill -0 PID 向某一进程发送一个无效的信号,如果该进程存在(能够接收信号),则执行 echo $? 得到 0 ;否则为 1 。由此可以知道进程是否存在。
...
=== 我是六道斑的分隔线 ===
书写优雅的shell脚本(五)- shell中(())双括号运算符
在使用 shell 的逻辑运算符”[]”使用时候,必须保证运算符与算数之间有空格。 四则运算也只能借助:let 、expr 等命令完成。 今天讲的双括号”(())”结构语句,就是对 shell 中算数及赋值运算的扩展。
5.1 语法
((表达式1,表达式2…))
特点:
- 在双括号结构中,所有表达式可以像 c 语言一样,如:a++、b-- 等。
- 在双括号结构中,所有变量可以不加入“$”符号前缀。
- 双括号可以进行逻辑运算,四则运算
- 双括号结构 扩展了 for 、while 、if 条件测试运算
- 支持多个表达式运算,各个表达式之间用“,”分开
5.2 使用实例
5.2.1 扩展四则运算
代码如下:
1 2 3 4 5 6 7 8 |
|
运行结果:
1 2 3 |
|
双括号结构之间支持多个表达式,然后加减乘除等 c 语言常用运算符都支持。如果双括号带 $ ,将获得表达式值,赋值给左边变量。
5.2.2 扩展逻辑运算
代码如下:
1 2 3 4 5 6 |
|
运行结果:
1 2 3 |
|
5.2.3 扩展条件测试运算(if)
代码如下:
1 2 3 4 5 6 7 8 |
|
运行结果:
1 |
|
5.2.4 扩展流程控制语句(逻辑关系式)
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
运算结果:
1 2 3 |
|
有了双括号运算符 [[]] 、[] 、test 等逻辑运算,以及 let 、expr 等就不是必须的了。
=== 我是如丧的分隔线 ===
书写优雅的shell脚本(六)- shell中的命令组合(&&、||、())
shell 在执行某个命令的时候,会返回一个返回值,该返回值保存在 shell 变量 $? 中。当 $? == 0 时,表示执行成功;当 $? == 1 时,表示执行失败。
有时候,下一条命令依赖前一条命令是否执行成功。比如在成功地执行一条命令之后再执行另一条命令,或者在一条命令执行失败后再执行另一条命令等。shell 提供了 && 和 || 来实现命令执行控制的功能,shell 将根据 && 或 || 前面命令的返回值来控制其后面命令的执行。
6.1 &&(命令执行控制)
语法格式如下:
command1 && command2 [&& command3 ...]
- 命令之间使用 && 连接,实现逻辑与的功能。
- 只有在 && 左边的命令返回真(命令返回值 $? == 0),&& 右边的命令才会被执行。
- 只要有一个命令返回假(命令返回值 $? == 1),后面的命令就不会被执行。
示例 1 :
1 |
|
示例 1 中的命令首先从 ~/workspace 目录复制 1.txt 文件到 ~ 目录;执行成功后,使用 rm 删除源文件;如果删除成功则输出提示信息。
6.2 ||(命令执行控制)
语法格式如下:
command1 || command2 [|| command3 ...]
- 命令之间使用 || 连接,实现逻辑或的功能。
- 只有在 || 左边的命令返回假(命令返回值 $? == 1),|| 右边的命令才会被执行。这和 c 语言中的逻辑或语法功能相同,即实现短路逻辑或操作。
- 只要有一个命令返回真(命令返回值 $? == 0),后面的命令就不会被执行。
示例 2 :
1 2 3 |
|
在示例 2 中,如果 ~/workspace 目录下不存在文件 1.txt,将输出提示信息。
示例 3 :
1 2 3 |
|
在示例 3 中,如果 ~/workspace 目录下存在文件 1.txt,将输出 success 提示信息;否则输出 fail 提示信息。
shell 提供了两种方法 () 和 {} 实现将几个命令合作一起执行,代替独立执行。这种方式并不能控制命令是否需要执行,仅是将多个单独的命令组合在一起执行,最终命令的返回值将由最后一条命令的返回值来决定。
6.3 ()(命令组合)
语法格式如下:
(command1;command2[;command3...])
- 一条命令需要独占一个物理行,如果需要将多条命令放在同一行,命令之间使用命令分隔符(;)分隔。执行的效果等同于多个独立的命令单独执行的效果。
- () 表示在当前 shell 中将多个命令作为一个整体执行。需要注意的是,使用 () 括起来的命令在执行前面都不会切换当前工作目录,也就是说命令组合都是在当前工作目录下被执行的,尽管命令中有切换目录的命令。
- 命令组合常和命令执行控制结合起来使用。
示例 4 :
1 2 3 4 |
|
在示例 4 中,如果目录 ~/workspace 下不存在文件 1.txt,则执行命令组合。
=== 我是晓松奇谈的分隔线 ===
linux bash shell中,单引号、 双引号,反引号(``)的区别及各种括号的区别
单引号和双引号
首先, 单引号和双引号,都是为了解决中间有空格的问题。
因为空格在 linux 中是作为一个很典型的分隔符使用,比如 string1=this is a string,这样执行就会报错。为了避免这个问题,因此就产生了单引号和双引号。区别在于,单引号将剥夺其中的所有字符的特殊含义,而双引号中的 '$'(参数替换)和'`'(命令替换)是例外。所以,两者基本上没有什么区别,除非在内容中遇到了参数替换符$和命令替换符`。
所以下面的结果:
1 2 3 4 5 6 |
|
所以,如果需要在双引号里面使用这两种符号本身,则需要使用反斜杠进行转义。
反引号
这个东西的用法,和 $() 是一样的。在执行一条命令时,会先将其中的 `xxx` 或 $(xxx) 中的语句当作命令优先执行,再将结果插入到原命令中执行原本的 shell 命令,例如:
1 |
|
会先执行 ls 得到 test.txt 等,再替换原命令为:
1 |
|
最后执行结果为
1 |
|
所以平时我们遇到的、把一堆命令的执行结果输出到一个变量中的情况,就需要利用这个命令替换符括起来。
这里又涉及到了一个问题,虽然不少系统工程师在使用替换功能时,喜欢使用反引号将命令括起来。但是根据 POSIX 规范,要求系统工程师采用的是 $(xxx) 的形式。所以,我们最好还是遵循这个规范,少用 `xxx`,多用 $(xxx) 。
小括号,中括号,和大括号的区别
先说说小括号和大括号的区别。这两者实际上都是用于表示“命令群组”的概念,也就是 command group 。
- () 把 command group 放到 subshell 去执行,也叫做 nested sub-shell。
- {} 则是在同一个 shell 內完成,也称为 non-named command group。
所以说,如果在 shel l里面执行“函数”,需要用到 {}。 不过,根据实测,test=$(ls -a) 可以正确执行,但是 test=${ls –a} 语法上面是有错误的。
另外,从网上摘录的两者区别如下:
- () 只是对一组命令重新开一个子 shell 进行执行
- {} 对一串命令在当前shell执行
- () 和 {} 都是把一串的命令包含在括号内,并且命令之间用分号隔开
- () 最后一个命令可以不用分号
- {} 最后一个命令要用分号
- {} 的第一个命令和左括号之间必须要有一个空格
- () 里的各命令不必和括号有空格
- () 和 {} 中括号里面的某个命令的重定向只影响该命令,但括号外的重定向则影响到括号里的所有命令
两个括号 (()),是代表算数扩展,就是对其包括的东西进行标准的算数计算。注意,不能算浮点数,如果需要算浮点数,需要用 bc 做。
至于中括号 [ ],感觉作用就是用来比较的(这里原作者理解的不好,[ ] 其实是 shell 中的 test 命令 )。比如放在 if 语句里面,while 语句里面,等等。