写好shell脚本的8个建议
这⼋个建议,来源于键者⼏年来编写 shell 脚本的⼀些经验和教训。事实上开始写的时候还不⽌这⼏条,后来思索再三,去掉⼏条⽆关痛痒的,最后剩下⼋条。
1. 指定bash
shell 脚本的第⼀⾏,#!之后应该是什么?如果拿这个问题去问别⼈,不同的⼈的回答可能各不相同。
我见过/usr/bin/env bash,也见过/bin/bash,还有/usr/bin/bash,还有/bin/sh,还有/usr/bin/env sh。这算是编程界的“'茴'字四种写法”了。
在多数情况下,以上五种写法都是等价的。但是,写过程序的⼈都知道:“少数情况”⾥往往隐藏着意想不到的坑。
如果系统的默认 shell 不是 bash 怎么办?⽐如某  发⾏版的某个版本,默认的 sh 就不是 bash。
如果系统的 bash 不是在 /usr/bin/bash怎么办?
我推荐使⽤ /usr/bin/env bash和 /bin/bash。前者通过env添加⼀个中间层,让env在$PATH中搜索bash;后者则是官⽅背书的,约定俗成的 bash 位置,/usr/bin/bash不过是指向它的⼀个符号链接。
2. set -e 和 set -x
OK,经过⼀番讨论,现在第⼀⾏定下来了。接下来该开始写第⼆⾏了吧?
且慢!在你开始构思并写下具体的代码逻辑之前,先插⼊⼀⾏set -e和⼀⾏set -x。
set -x会在执⾏每⼀⾏ shell 脚本时,把执⾏的内容输出来。它可以让你看到当前执⾏的情况,⾥⾯涉及的变量也会被替换成实际的值。set -e会在执⾏出错时结束程序,就像其他语⾔中的“抛出异常”⼀样。(准确说,不是所有出错的时候都会结束程序,见下⾯的注)
注:set -e结束程序的条件⽐较复杂,在man bash⾥⾯,⾜⾜⽤了⼀段话描述各种情景。⼤多数执⾏都会在出错时退出,除⾮ shell 命令位于以下情况:
1. ⼀个 pipeline 的⾮结尾部分,⽐如error | ok
2. ⼀个组合语句的⾮结尾部分,⽐如ok && error || other
3. ⼀连串语句的⾮结尾部分,⽐如error; ok
4. 位于判断语句内,包括test、if、while等等。
这两个组合在⼀起⽤,可以在 debug 的时候替你节省许多时间。出于防御性编程的考虑,有必要在写第⼀⾏具体的代码之前就插⼊它们。扪⼼⾃问,写代码的时候能够⼀次写对的次数有多少?⼤多数代码,在提交之前,通常都经历过反复调试修改的过程。与其在焦头烂额之际才引⼊这两个配置,不如⼀开始就给 debug 留下余地。在代码终于可以提交之后,再考虑是否保留它们也不迟。
3. 带上shellcheck
好了,现在我已经有了三⾏(样板)代码,具体的业务逻辑⼀⾏都没写呢。是不是该开始写了?
且慢!⼯欲善其事,必先利其器。这次,我就介绍⼀个 shell 脚本编写神器:
说来惭愧,虽然写了⼏年 shell 脚本,有些语法我还是记不清楚。这时候就要依仗 shellcheck 指点⼀下了。
shellcheck 除了可以提醒语法问题以外,还能检查出 shell 脚本编写常见的 bad code。本来我的N条建议⾥⾯,还有⼏条是关于这些 bad code 的,不过考虑到 shellcheck 完全可以发掘出这些问题,于是忍痛把它们都剔除在外了。毫⽆疑问,使⽤ shellcheck 给我的 shell 编写技能带来了巨⼤的飞跃。
所谓“站在巨⼈的肩膀上”,虽然我们这些新兵蛋⼦,技能不如⽼兵们强,但是我们可以在装备上赶上对⽅啊!动动⼿安装⼀下,就能结识⼀个循循善诱的“⽼师”,何乐⽽不为?顺便⼀提,shellcheck 居然是
⽤ haskell 写的。谁说 haskell 只能⽤来?
4. 变量展开
在 shell 脚本中,偶尔可以看到这样的做法:
echo $xxx | awk/sed/
看起来⼤张形势的样⼦,其实不过是想修改⼀个变量的值。杀鸡何必⽤⽜⼑?bash内建的变量展开机制已经⾜以满⾜你各种需求!还是⽼⽅法, read the f**k manaul! man bash然后搜索Parameter Expansion,下⾯就是你想要的技巧。键者也写过⼀篇相关的⽂章,希望能助上⼀臂之⼒:
=======================================================================
awk 我们可以利⽤awk命令,将⼀些⽂本整理成我们想要的样⼦,⽐如把⼀些⽂本整理成"表"的
sed  更适合编辑匹配到的⽂本
grep
cut
========================================================================
5. 注意local
随着代码越写越多,你开始把重复的逻辑提炼成函数。有可能你会掉到bash的⼀个坑⾥。在bash,如果不加 local 限定词,变量默认都是全局的。变量默认全局——这跟 js 和 lua 相似;但相较⽽⾔,很少有 bash 教程⼀开始就告知你这个事实。在顶级作⽤域⾥,是否是全局变量并不重要。但是在函数⾥⾯,声明⼀个全局变量可能会污染到其他作⽤域(尤其在你根本没有注意到这⼀点的情况下)。所以,对于在函数内声明的变量,请务必记得加上 local 限定词。
6. trap信号
如果你写过稍微复杂点的在后台运⾏的程序,应该知道 posix 标准⾥⾯“信号”是什么⼀回事。如果不知道,直接看下⼀段。像其他语⾔⼀样,shell 也⽀持处理信号。trap sighandler INT可以在接收到 SIGINT 时调⽤ sighandler 函数。捕获其他信号的⽅式以此类推。
不过 trap 的主要应⽤场景可不是捕获哪个信号。trap命令⽀持“捕获”许多不同的流程——准确来说,允许⽤户给特定的流程注⼊函数调⽤。其中最为常⽤的是trap func EXIT和trap func ERR。
trap func EXIT允许在脚本结束时调⽤函数。由于⽆论正常退出抑或异常退出,所注册的函数都能得以调
⽤,在需要调⽤⼀个清理函数的场景下,我都是⽤它注册清理函数,⽽不是简单地在脚本结尾调⽤清理函数。
trap func ERR允许在运⾏出错时调⽤函数。⼀个常⽤的技法是,使⽤全局变量ERROR存储错误信息,然后在注册的函数中根据存储的值完成对应的错误报告。把原本四分五裂的错误处理逻辑集中到⼀处,有时候会起奇效。不过要记住,程序异常退出时,既会调⽤EXIT注册的函数,也会调⽤ERR注册的函数。
===========================================
SIGTERM和SIGINT的含义
2016年06⽉08⽇ 08:58:32 阅读数:5737
SIGHUP    终⽌进程    终端线路挂断
SIGINT    终⽌进程    中断进程
SIGQUIT  建⽴CORE⽂件终⽌进程,并且⽣成core⽂件
SIGILL  建⽴CORE⽂件      ⾮法指令
SIGTRAP  建⽴CORE⽂件      跟踪⾃陷
SIGBUS  建⽴CORE⽂件      总线错误
SIGSEGV  建⽴CORE⽂件      段⾮法错误
SIGFPE  建⽴CORE⽂件      浮点异常
SIGIOT  建⽴CORE⽂件      执⾏I/O⾃陷
SIGKILL  终⽌进程    杀死进程
SIGPIPE  终⽌进程    向⼀个没有读进程的管道写数据
SIGALARM  终⽌进程    计时器到时
SIGTERM  终⽌进程    软件终⽌信号
SIGSTOP  停⽌进程    ⾮终端来的停⽌信号
SIGTSTP  停⽌进程    终端来的停⽌信号
SIGCONT  忽略信号    继续执⾏⼀个停⽌的进程
SIGURG  忽略信号    I/O紧急信号
SIGIO    忽略信号    描述符上可以进⾏I/O
SIGCHLD  忽略信号    当⼦进程停⽌或退出时通知⽗进程
SIGTTOU  停⽌进程    后台进程写终端
SIGTTIN  停⽌进程    后台进程读终端
SIGXGPU  终⽌进程    CPU时限超时
SIGXFSZ  终⽌进程    ⽂件长度过长
SIGWINCH  忽略信号    窗⼝⼤⼩发⽣变化
SIGPROF  终⽌进程    统计分布图⽤计时器到时
SIGUSR1  终⽌进程    ⽤户定义信号1
SIGUSR2  终⽌进程    ⽤户定义信号2
SIGVTALRM 终⽌进程    虚拟计时器到时
1) SIGHUP 本信号在⽤户终端连接(正常或⾮正常)结束时发出, 通常是在终端的控
制进程结束时, 通知同⼀session内的各个作业, 这时它们与控制终端
不再关联.
2) SIGINT 程序终⽌(interrupt)信号, 在⽤户键⼊INTR字符(通常是Ctrl-C)时发出
3) SIGQUIT 和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产⽣core⽂件, 在这个意义上类似于⼀个程序错误信
号.
4) SIGILL 执⾏了⾮法指令. 通常是因为可执⾏⽂件本⾝出现错误, 或者试图执⾏
数据段. 堆栈溢出时也有可能产⽣这个信号.
5) SIGTRAP 由断点指令或其它trap指令产⽣. 由debugger使⽤.
6) SIGABRT 程序⾃⼰发现错误并调⽤abort时产⽣.
6) SIGIOT 在PDP-11上由iot指令产⽣, 在其它机器上和SIGABRT⼀样.
7) SIGBUS ⾮法地址, 包括内存地址对齐(alignment)出错. eg: 访问⼀个四个字长
的整数, 但其地址不是4的倍数.linuxshell脚本怎么运行
8) SIGFPE 在发⽣致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢
出及除数为0等其它所有的算术的错误.
9) SIGKILL ⽤来⽴即结束程序的运⾏. 本信号不能被阻塞, 处理和忽略.
10) SIGUSR1 留给⽤户使⽤
11) SIGSEGV 试图访问未分配给⾃⼰的内存, 或试图往没有写权限的内存地址写数据.
12) SIGUSR2 留给⽤户使⽤
13) SIGPIPE Broken pipe
14) SIGALRM 时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使⽤该
信号.
15) SIGTERM 程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理. 通常⽤来要求程序⾃⼰正常退出. shell命令kill缺省产⽣这
个信号.
17) SIGCHLD ⼦进程结束时, ⽗进程会收到这个信号.
18) SIGCONT 让⼀个停⽌(stopped)的进程继续执⾏. 本信号不能被阻塞. 可以⽤
⼀个handler来让程序在由stopped状态变为继续执⾏时完成特定的
⼯作. 例如, 重新显⽰提⽰符
19) SIGSTOP 停⽌(stopped)进程的执⾏. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执⾏. 本信号不能被阻塞, 处理或忽略.
20) SIGTSTP 停⽌进程的运⾏, 但该信号可以被处理和忽略. ⽤户键⼊SUSP字符时(通常是Ctrl-Z)发出这
个信号
21) SIGTTIN 当后台作业要从⽤户终端读数据时, 该作业中的所有进程会收到SIGTTIN
信号. 缺省时这些进程会停⽌执⾏.
22) SIGTTOU 类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.
23) SIGURG 有"紧急"数据或out-of-band数据到达socket时产⽣.
24) SIGXCPU 超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/
改变
25) SIGXFSZ 超过⽂件⼤⼩资源限制.
26) SIGVTALRM 虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占⽤的CPU时间.
27) SIGPROF 类似于SIGALRM/SIGVTALRM, 但包括该进程⽤的CPU时间以及系统调⽤的
时间.
28) SIGWINCH 窗⼝⼤⼩改变时发出.
29) SIGIO ⽂件描述符准备就绪, 可以开始进⾏输⼊/输出操作.
30) SIGPWR Power failure
有两个信号可以停⽌进程:SIGTERM和SIGKILL。 SIGTERM⽐较友好,进程能捕捉这个信号,根据您的需要来关闭程序。在关闭程序之前,您可以结束打开的记录⽂件和完成正在做的任务。在某些情况下,假如进程正在进⾏作业⽽且不能中断,那么进程可以忽略这个SIGTERM信号。
对于SIGKILL信号,进程是不能忽略的。这是⼀个 “我不管您在做什么,⽴刻停⽌”的信号。假如您发送SIGKILL信号给进程,Linux就将进程停⽌在那⾥。
================================================
--------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------------------
linux下采⽤trap对信号进⾏捕捉
在Linux中,trap命令主要⽤于接收信号并采取⾏动,信号是异步发送到⼀个程序的事件,在默认情况下,可以终⽌⼀个程序,trap命令原型如下:
trap command signal
signal是指接收到的信号,command是接收到该信号采取的⾏动。如下为两种简单的信号。
信号
说明
INT(2)
Ctrl + C
QUIT(3)
Ctrl + \
⽰例代码如下
1. [root@localhost shell]# cat -n trap.sh
2. 1 #!/bin/bash
3. 2 trap "echo 'sorry! I habe trapped Ctrl+C'" SIGINT SIGTERM
4. 3 echo this is a test program
5. 4 count=1
6. 5 while [ $count -le 10 ]
7. 6 do
8. 7 echo "LOOP #$count"
9. 8 sleep 5
0. 9 count=$[ $count + 1]
1. 10 done
2. 11 echo this is the end of the test program
3. [root@localhost shell]#
运⾏时按下Ctrl+C结果如图:
1. [root@localhost shell]# ./trap.sh
2. this is a test program
3. LOOP #1
4. LOOP #2
5. ^Csorry! I habe trapped Ctrl+C
6. LOOP #3
7. LOOP #4
8. ^Csorry! I habe trapped Ctrl+C
9. LOOP #5
0. LOOP #6
1. LOOP #7
2. LOOP #8
3. ^Csorry! I habe trapped Ctrl+C
4. LOOP #9
5. LOOP #10
6. this is the end of the test program
7. [root@localhost shell]#

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。