如何编写可靠shell_Linux操作

 

前言

作为互联网从业者,经常需要和Linux打交道,当然不可避免的也要写一些shell,无论是进行CI/CD流水线搭建、数据处理、抑或是进行系统管理,随处可见大量shell脚本的影子。shell有一个很大的特点,也不知道该称之为优点还是缺点,就是它的语法相当灵活,100个人中就有100种写法或是代码风格,究竟该怎么写,可能会逼死强迫症。

为什么是“可靠shell”呢?大家可能都有过shell语言在某些情况下造成的“灾难”,也许是因为我们使用了非预期的变量、产生了非预期的返回,却没有及时判断处理导致了不可预知的问题。

于是花了些功夫查阅资料,给团队做了一次分享,始有此文。在这里,笔者无视那些众多的shell解释器,就以Linux标准的bash为例,斗胆整理了了一份关于shell的部分编写建议作为自己写shell时的参考指南,还请各位读者抱着批判性思维来审阅。

 

开头指定bash

指定bash的方式有很多,不过建议大家使用下面两种中的一种:

如何编写可靠shell_Linux操作_02

 

有几点说明:

1、运行./a.sh时,当没有指定shebang时,就会默认用$SHELL指定的解释器,否则就会用shebang指定的解释器

2、#!/bin/bash 的方式限制了代码注入的可能,在某些情况下更安全

3、#!/usr/bin/env bash 的方式通过添加env中间层,使得可以在$PATH中搜索bash,提供灵活性、适应性

 

用双引号包围变量

考虑如下代码段:

如何编写可靠shell_Linux操作_03

 

运行会报错,因为等号前后字符串个数不一致。正确的做法是如下代码:

如何编写可靠shell_笔记_04

 

要小心命令行参数中的空格。如果变量要放到if语句中,最好用双引号包围,其他情况下,包围变量也是一个不错的实践。当然,在双引号中继续用{}大括号包围变量,比如"${filename}" ,也是推荐的写法。

 

全部代码进函数

建议除公共部分外,所有的代码都封装进函数,即使只有一个函数,也定义一个main函数。

如何编写可靠shell_Linux操作_05

 

最常见到的不规范的写法就是大家的shell全都不在函数中,一条条命令顺序执行(笔者以前也经常这么写)。

定义函数有几种方式:

如何编写可靠shell_Linux操作_06

 

建议使用标准写法(第一种)。Shell函数在定义时不能指明参数,只在调用时可以传递参数,且传递什么则接收什么。上面的"$@"则是接收命令行参数的写法。

 

使用readonly定义常量

如何编写可靠shell_Linux操作_07

 

使用readonly修饰的变量定义会变成只读变量,无法在脚本中被修改,更加安全。

关注变量作用域

如何编写可靠shell_Linux操作_08

 

1、Shell中默认变量作用域为全局(无论定义在外层还是函数中)

2、强烈建议定义变量时用local、readonly修饰(定义在函数内),有充足理由时可以使用declare(如需定义整型变量)

3、如果必须定义全局变量,则建议全局变量大写

 

警惕未被初始化的变量

如何编写可靠shell_Linux操作_09

 

如果运行上面的脚本,参数为空的话,你的根目录的data目录就被删掉了。可以使用nounset标志来防止这种意外情况的发生:

如何编写可靠shell_Linux操作_10

 

1、set –o nounset的另一种表达方式:set -u

2、当使用了未初始化的变量时,设置set -o nounset,可以让程序强制退出

当然,上面例子只是为说明问题,脚本可不能这么写,太危险。

 

让代码执行可追踪

不多说,使用set -o xtrace可达到该目的,将每行的执行命令输出。也可以简写为set -x。常用于调试场景,也可以在执行shell时使用sh -x 的方式调试脚本。

 

防止错误滚雪球

假如这么写shell(假设a.txt不存在):

如何编写可靠shell_笔记_11

 

那么仍然会尝试删除a.txt。

如果我们想判断下上一步的执行结果再决定下一步行动,常用的做法可能是这样:

如何编写可靠shell_Linux操作_12

 

我们通过$?的值来判断上一步的状态,进而决定是继续删除还是直接退出。

还有另外一种方式也可以达到目的:

如何编写可靠shell_Linux操作_13

 

1、set -o errexit的另一种表达方式:set -e

2、使用set -o errexit,一但有任何一个语句返回非0值,则退出bash,从而尽早捕获错误

3、此时无法使用$?获取命令执行状态,因为bash无法获得任何非0返回值

4、如果需要让程序即使出错也继续执行,可以在可能出错的语句追加" || true"

 

学会查路径

强烈建议在脚本开头定义基础目录,如下:

如何编写可靠shell_笔记_14

 

1、基于当前脚本执行路径,指定其他路径;

2、在每个脚本前设置当前工作区、脚本名、工程根目录的只读变量是一个好习惯

3、让脚本在任何目录下都可以正常执行(脚本中所有位置全部使用决定路径,尽量少使用相对路径;)

 

巧用shift

如何编写可靠shell_Linux操作_15

 

上面的func2使用了shift,使得所有命令行参数都可以通过$1读取。再举个更实用的例子:

如何编写可靠shell_笔记_16

 

假设该文件命名为test.sh,我们运行时使用:sh test.sh --file a.txt --module module_a 的方式,程序就可以精确获取到每个参数,用于后续的逻辑处理。

 

封装一些常用指令

假如我们经常需要检查命令执行状态,就可以封装一个函数:

如何编写可靠shell_笔记_17

 

以后在需要的地方调用该函数即可。

 

提供help信息

不多说,脚本最好提供一个help函数,当用户输入参数异常时能够及时给出反馈。

 

切换目录的几种方式

假如我们需要临时在某个路径下执行一些指令,为了不改变主程序的执行路径,可以有几种方式:

如何编写可靠shell_Linux操作_18

 

巧用trap信号

如何编写可靠shell_Linux操作_19

 

trap func EXIT允许在脚本结束时调用函数,用它注册清理函数。

 

让脚本可以单独运行任意一个函数

如何编写可靠shell_笔记_20

 

如上编写的脚本(假设test.sh),我们在运行的时候,可以使用sh test.sh --eval start 来单独运行start方法。

 

一些额外的小tip

1、在条件判断时,尽量使用双中括号"[["而非单中括号"["。单中括号是一个Linux命令,每次使用都会fork一个子进程。双中括号是shell关键字,更加强大,可完全替代单中括号

2、判断时,有个小技巧:[[ "z${var}" = "z" ]] 加入任意前导字符(此处是z)可以防止var变量为空时脚本报错

3、利用/dev/null过滤不需要的输出信息:$ command > /dev/null 2>&1

4、变量可以习惯性使用{}包围,以防意外情况,且用双引号包围是个好习惯,如 "${var}"

5、把then,do等和if、while或者for写在同一行,不换行

6、一行太长时使用 \ 进行换行,换行原则是整齐美观

7、禁止直接操作$1、$2等参数,除非这些变量只用一次

8、整数运算使用$(()),如 echo $((3+4)) ;小数运算使用bc计算器,如 echo "scale=2; 5/3" |bc

9、尽量使用$()将命令结果赋值给变量,而非使用反引号

10、尽量使用绝对路径,不易出错

11、shell脚本main函数接收参数时,尽量使用main "$@" 的形式。以下是各种形式传参的结果:

……

如何编写可靠shell_Linux操作_21

 

你的脚本可以这样开始

如何编写可靠shell_笔记_22

 

后记

shell作为一门很灵活的语言,编写的规范性也越发显得重要。可靠性作为软件质量模型中的第二个特性,很多时候都体现在代码层面。文章稍长,为了加深印象,使用了较多代码段,提到的建议也只是冰山一角,建议感兴趣的读者有机会自己写代码尝试下,也欢迎提出宝贵意见。