FYIRH 发表于 2021-12-26 23:22:48

运维必知必会:Bash Shell 脚本的实践指南

本帖最后由 FYIRH 于 2021-12-26 23:47 编辑

前言

与其它的编码规范一样,这里所讨论的不仅仅是编码格式美不美观的问题, 同时也讨论一些约定及编码标准。这份文档主要侧重于我们所普遍遵循的规则,对于那些不是明确强制要求的,我们尽量避免提供意见。

为什么要有编码规范

编码规范对于程序员而言尤为重要,有以下几个原因:


[*]一个软件的生命周期中,80%的花费在于维护
[*]几乎没有任何一个软件,在其整个生命周期中,均由最初的开发人员来维护
[*]编码规范可以改善软件的可读性,可以让程序员尽快而彻底地理解新的代码
[*]如果你将源码作为产品发布,就需要确任它是否被很好的打包并且清晰无误,一如你已构建的其它任何产品
[*]编码规范原则


本文档中的准则致力于最大限度达到以下原则:


[*]正确性
[*]可读性
[*]可维护性
[*]可调试性
[*]一致性
[*]美观


尽管本文档涵盖了许多基础知识,但应注意的是,没有编码规范可以为我们回答所有问题,开发人员始终需要再编写完代码后,对上述原则做出正确的判断。

代码规范等级定义

[*]可选(Optional):用户可参考,自行决定是否采用;
[*]推荐(Preferable):用户理应采用,但如有特殊情况,可以不采用;
[*]必须(Mandatory):用户必须采用(除非是少数非常特殊的情况,才能不采用);
注:未明确指明的则默认为必须(Mandatory)
主要参考如下文档:
[*]Google Shell Style Guide
[*]Bash Hackers Wiki
源文件

基础

使用场景

仅建议Shell用作相对简单的实用工具或者包装脚本。因此单个shell脚本内容不宜太过复杂。

在选择何时使用shell脚本时时应遵循以下原则:


[*]如主要用于调用其他工具且需处理的数据量较少,则shell是一个选择
[*]如对性能十分敏感,则更推荐选择其他语言,而非shell
[*]如需处理相对复杂的数据结构,则更推荐选择其他语言,而非shell
[*]如脚本内容逐渐增长且有可能出现继续增长的趋势,请尽早使用其他语言重写

文件名

可执行文件不建议有扩展名,库文件必须使用.sh作为扩展名,且应是不可执行的。

执行一个程序时,无需知道其编写语言,且shell脚本并不要求具有扩展名,所以更倾向可执行文件没有扩展名。

而库文件知道其编写语言十分重要,使用.sh作为特定语言后缀的扩展名,可以和其他语言编写的库文件加以区分。

文件名要求全部小写, 可以包含下划线_或连字符-, 建议可执行文件使用连字符,库文件使用下划线。

正例:反例:
文件编码

源文件编码格式为UTF-8。避免不同操作系统对文件换行处理的方式不同,一律使用LF。

单行长度

每行最多不超过120个字符。每行代码最大长度限制的根本原因是过长的行会导致阅读障碍,使得缩进失效。

除了以下两种情况例外:


[*]导入模块语句
[*]注释中包含的URL

如出现长度必须超过120个字符的字符串,应尽量使用here document或者嵌入的换行符等合适的方法使其变短。示例:
空白字符

除了在行结束使用换行符,空格是源文件中唯一允许出现的空白字符。

[*]字符串中的非空格空白字符,使用转义字符
[*]不允许行前使用tab缩进,如果使用tab缩进,必须设置1个tab为4个空格
[*]不应在行尾出现没有意义的空白字符
垃圾清理(推荐)
对从来没有用到的或者被注释的方法、变量等要坚决从代码中清理出去,避免过多垃圾造成干扰。
结构

使用bash
Bash 是唯一被允许使用的可执行脚本shell。
可执行文件必须以 #!/bin/bash开始。请使用set来设置shell的选项,使得用bash <script_name>调用你的脚本时不会破坏其功能。
限制所有的可执行shell脚本为bash使得我们安装在所有计算机中的shell语言保持一致性。正例:
反例:

许可证或版权信息(推荐)许可证与版权信息需放在源文件的起始位置。例如:

缩进

块缩进
每当开始一个新的块,缩进增加4个空格(不能使用\t字符来缩进)。当块结束时,缩进返回先前的缩进级别。缩进级别适用于代码和注释。

管道
如果一行容不下整个管道操作,那么请将整个管道操作分割成每行一个管段。


如果一行容得下整个管道操作,那么请将整个管道操作写在同一行,管道左右应有空格。

否则,应该将整个管道操作分割成每行一段,管道操作的下一部分应该将管道符放在新行并且缩进4个空格。

这适用于管道符 | 以及逻辑运算 || 和 &&。正例:




反例:



循环
请将 ; do , ; then 和 while , for , if 放在同一行。
shell中的循环略有不同,但是我们遵循跟声明函数时的大括号相同的原则。即:; do , ; then 应该和 while/for/if 放在同一行。else 应该单独一行。结束语句应该单独一行且跟开始语句缩进对齐。
正例:

反例:





case语句
通过4个空格缩进可选项。可选项中的多个命令应该被拆分成多行,模式表达式、操作和结束符 ;; 在不同的行。
匹配表达式比 case 和 esac 缩进一级。多行操作要再缩进一级。模式表达式前面不应该出现左括号。避免使用 ;& 和 ;;& 符号。示例:

只要整个表达式可读,简单的单行命令可以跟模式和 ;; 写在同一行。当单行容不下操作时,请使用多行的写法。单行示例:
函数位置
将文件中所有的函数统一放在常量下面。不要在函数之间隐藏可执行代码。
如果你有函数,请将他们统一放在文件头部。只有includes, set 声明和常量设置可能在函数声明之前完成。不要在函数之间隐藏可执行代码。如果那样做,会使得代码在调试时难以跟踪并出现意想不到的结果。
主函数main
对于包含至少了一个其他函数的足够长的脚本,建议定义一个名为 main 的函数。对于功能简单的短脚本, main函数是没有必要的。
为了方便查找程序的入口位置,将主程序放入一个名为 main 的函数中,作为最底部的函数。这使其和代码库的其余部分保持一致性,同时允许你定义更多变量为局部变量(如果主代码不是一个函数就不支持这种做法)。文件中最后的非注释行应该是对 main 函数的调用:

注释

代码注释的基本原则:


[*]注释应能使代码更加明确
[*]避免注释部分的过度修饰
[*]保持注释部分简单、明确
[*]在编码以前就应开始写注释
[*]注释应说明设计思路而不是描述代码的行为
[*]注释与其周围的代码在同一缩进级别,#号与注释文本间需保持一个空格以和注释代码进行区分。

文件头

每个文件的开头是其文件内容的描述。除版权声明外,每个文件必须包含一个顶层注释,对其功能进行简要概述。

例如:




功能注释

主体脚本中除简洁明了的函数外都必须带有注释。库文件中所有函数无论其长短和复杂性都必须带有注释。

这使得其他人通过阅读注释即可学会如何使用你的程序或库函数,而不需要阅读代码。

所有的函数注释应该包含:


[*]函数的描述
[*]全局变量的使用和修改
[*]使用的参数说明
[*]返回值,而不是上一条命令运行后默认的退出状态


例如:
TODO注释

对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 TODO 注释.

TODO 注释要使用全大写的字符串 TODO, 在随后的圆括号里写上你的名字,邮件地址, bug ID, 或其它身份标识和与这一 TODO 相关的 issue。

主要目的是让添加注释的人 (也是可以请求提供更多细节的人) 可根据规范的TODO 格式进行查找。添加 TODO 注释并不意味着你要自己来修正,因此当你加上带有姓名的 TODO 时, 一般都是写上自己的名字。这与C++ Style Guide中的约定相一致。

例如:



命名

函数名

使用小写字母,并用下划线分隔单词。使用双冒号::分隔包名。函数名之后必须有圆括号。如果你正在写单个函数,请用小写字母来命名,并用下划线分隔单词。

如果你正在写一个包,使用双冒号 :: 来分隔包名。函数名和圆括号之间没有空格,大括号必须和函数名位于同一行。当函数名后存在 () 时,关键词 function 是多余的,建议不带 function 的写法,但至少做到同一项目内风格保持一致。正例:




反例:



变量名

规则同函数名一致。
循环中的变量名应该和正在被循环的变量名保持相似的名称。示例:

常量和环境变量名
全部大写,用下划线分隔,声明在文件的顶部。
常量和任何导出到环境中的变量都应该大写。示例:

有些情况下首次初始化及常量(例如,通过getopts),因此,在getopts中或基于条件来设定常量是可以的,但之后应该立即设置其为只读。值得注意的是,在函数中使用 declare 对全局变量无效,所以推荐使用 readonly 和 export 来代替。示例:

只读变量
使用 readonly 或者 declare -r 来确保变量只读。
因为全局变量在shell中广泛使用,所以在使用它们的过程中捕获错误是很重要的。当你声明了一个变量,希望其只读,那么请明确指出。示例:

局部变量

每次只声明一个变量,不要使用组合声明,比如a=1 b=2;

使用 local 声明特定功能的变量。声明和赋值应该在不同行。

必须使用 local 来声明局部变量,以确保其只在函数内部和子函数中可见。这样可以避免污染全局名称空间以及避免无意中设置可能在函数外部具有重要意义的变量。

当使用命令替换进行赋值时,变量声明和赋值必须分开。因为内建的 local 不会从命令替换中传递退出码。正例:



反例:



异常与日志

异常
使用shell返回值来返回异常,并根据不同的异常情况返回不同的值。
日志
所有的错误信息都应被导向到STDERR,这样将有利于出现问题时快速区分正常输出和异常输出。
建议使用与以下函数类似的方式来打印正常和异常输出:

编程实践(持续分类并完善)

变量扩展(推荐)
通常情况下推荐为变量加上大括号如 "${var}" 而不是 "$var" ,但具体也要视情况而定。
以下按照优先顺序列出建议:

[*]与现有代码保持一致
[*]单字符变量在特定情况下才需要被括起来
[*]使用引号引用变量,参考下一节:变量引用
详细示例如下:正例:
反例:




变量引用(推荐)
变量引用通常情况下应遵循以下原则:

[*]默认情况下推荐使用引号引用包含变量、命令替换符、空格或shell元字符的字符串
[*]在有明确要求必须使用无引号扩展的情况下,可不用引号
[*]字符串为单词类型时才推荐用引号,而非命令选项或者路径名
[*]不要对整数使用引号
[*]特别注意 [[ 中模式匹配的引号规则
[*]在无特殊情况下,推荐使用 $@ 而非 $*
以下通过示例说明:
命令替换
使用 $(command) 而不是反引号。
因反引号如果要嵌套则要求用反斜杠转义内部的反引号。而 $(command) 形式的嵌套无需转义,且可读性更高。正例:

反例:
条件测试
使用 [[ ... ]] ,而不是 [ , test , 和 /usr/bin/[ 。
因为在 [[ 和 ]] 之间不会出现路径扩展或单词切分,所以使用 [[ ... ]] 能够减少犯错。且 [[ ... ]] 支持正则表达式匹配,而 [ ... ] 不支持。参考以下示例:

字符串测试
尽可能使用变量引用,而非字符串过滤。
Bash可以很好的处理空字符串测试,请使用空/非空字符串测试方法,而不是过滤字符,让代码具有更高的可读性。正例:

反例:
正例:
反例:
正例:
反例:
文件名扩展
当进行文件名的通配符扩展时,请指定明确的路径。
当目录中有特殊文件名如以 - 开头的文件时,使用带路径的扩展通配符 ./* 比不带路径的 *要安全很多。
慎用eval
应该避免使用eval。
Eval在用于分配变量时会修改输入内容,但设置变量的同时并不能检查这些变量是什么。反例:

慎用管道连接 while 循环
请使用进程替换或者for循环,而不是通过管道连接while循环。
这是因为在管道之后的while循环中,命令是在一个子shell中运行的,因此对变量的修改是不能传递给父shell的。
这种管道连接while循环中的隐式子shell使得bug定位非常困难。反例:

如果你确定输入中不包含空格或者其他特殊符号(通常不是来自用户输入),则可以用for循环代替。例如:

使用进程替换可实现重定向输出,但是请将命令放入显式子 shell,而非 while 循环创建的隐式子 shell。例如:

检查返回值
总是检查返回值,且提供有用的返回值。
对于非管道命令,使用 $? 或直接通过 if 语句来检查以保持其简洁。
例如:
内建命令和外部命令

当内建命令可以完成相同的任务时,在shell内建命令和调用外部命令之间,应尽量选择内建命令。

因内建命令相比外部命令而言会产生更少的依赖,且多数情况调用内建命令比调用外部命令可以获得更好的性能(通常外部命令会产生额外的进程开销)。正例:




反例:



文件加载
加载外部库文件不建议用使用.,建议使用source,已提升可阅读性。正例:

反例:
内容过滤与统计
除非必要情况,尽量使用单个命令及其参数组合来完成一项任务,而非多个命令加上管道的不必要组合。常见的不建议的用法例如:cat和grep连用过滤字符串; cat和wc连用统计行数; grep和wc连用统计行数等。正例:
反例:


正确使用返回与退出
除特殊情况外,几乎所有函数都不应该使用exit直接退出脚本,而应该使用return进行返回,以便后续逻辑中可以对错误进行处理。正例:
反例:
(转自高效运维)


页: [1]
查看完整版本: 运维必知必会:Bash Shell 脚本的实践指南