背景

使用哪一种Shell

可执行文件必须以 #!/bin/bash 和最小数量的标志开始。请使用 set 来设置shell的选项,使得用 <script_name>调用你的脚本时不会破坏其功能。

推荐使用:

1
#!/usr/bin/env bash

env一般固定在/usr/bin目录下,而其余解释器的安装位置就相对不那么固定。

限制所有的可执行Shell脚本为bash使得我们安装在所有计算机中的shell语言保持一致性。

无论你是为什么而编码,对此唯一例外的是当你被迫时可以不这么做的。其中一个例子是Solaris SVR4包,编写任何脚本都需要用纯Bourne shell

1
2
[root@test ~]# echo $SHELL
/bin/bash

什么时候使用Shell

使用Shell需要遵守的一些准则:

  • 如果你主要是在调用其他的工具并且做一些相对很小数据量的操作,那么使用Shell来完成任务是一种可接受的选择。
  • 如果你在乎性能,那么请选择其他工具,而不是使用Shell。
  • 如果你发现你需要使用数据而不是变量赋值(如 ${PHPESTATUS} ),那么你应该使用Python脚本。
  • 如果你将要编写的脚本会超过100行,那么你可能应该使用Python来编写,而不是Shell。

请记住,当脚本行数增加,尽早使用另外一种语言重写你的脚本,以避免之后花更多的时间来重写。

注释

Bash只支持单行注释,使用#开头的都被当作注释语句。

顶层注释

每个文件必须包含一个顶层注释,对其内容进行简要概述。版权声明和作者信息是可选的。
例如:

1
2
3
4
5
#!/usr/bin/env bash
# Author: Rohn
# Version: 1.0
# Created Time: 2020/06/06
# Perform hot backups of MySQL databases.
  • 第1行,指明解释器,使用bash

#!叫做”Shebang“或者”Sha-bang”(Unix术语中,#号通常称为sharp,hash或mesh;而!则常常称为bang),指明了执行这个脚本文件的解释程序。当然,如果使用bash test.sh这样的命令来执行脚本,那么#!这一行将会被忽略掉。

  • 第2-5行,分别为作者、版本号、创建时间、功能说明。

功能注释

任何不是既明显又短的函数都必须被注释。任何库函数无论其长短和复杂性都必须被注释。

其他人通过阅读注释(和帮助信息,如果有的话)就能够学会如何使用你的程序或库函数,而不需要阅读代码。

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

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

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env bash
# Author: Rohn
# Version: 1.0
# Created Time: 2020/06/06
# Perform hot backups of Oracle databases.

export PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin'

#######################################
# Cleanup files from the backup dir
# Globals:
# BACKUP_DIR
# ORACLE_SID
# Arguments:
# None
# Returns:
# None
#######################################
cleanup() {
...
}

TODO注释

TODOs应该包含全部大写的字符串TODO,接着是括号中你的用户名。冒号是可选的。最好在TODO条目之后加上bug或者ticket的序号。

例如:

1
# TODO(mrmonkey): Handle the unlikely edge cases (bug ####)

格式

缩进

缩进两个空格,没有制表符。例如:

1
2
3
if [ a > 1 ];then
echo '${a} > 1'
fi

行的长度和长字符串

行的最大长度为80个字符。例如:

1
2
3
4
5
6
7
8
9
# DO use 'here document's
cat <<END;
I am an exceptionally long
string.
END

# Embedded newlines are ok too
long_string="I am an exceptionally
long string."

管道

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

应该将整个管道操作分割成每行一个管段,管道操作的下一部分应该将管道符放在新行并且缩进2个空格。这适用于使用管道符|的合并命令链以及使用||&&的逻辑运算链。

例如:

1
2
3
4
5
6
7
8
# All fits on one line
command1 | command2

# Long commands
command1 \
| command2 \
| command3 \
| command4

循环

if-else语句

if; then放在同一行,;后空一格,else单独一行,fi单独一行,并与if垂直对齐。即:

1
2
3
4
5
if condition; then
statement(s)
else
statement(s)
fi

for-do和while-do语句

while/for; do放在同一行,donewhile/for垂直对齐,即:

1
2
3
4
5
6
7
8
9
# while structure
while condition; do
statement(s)
done

# for structure
for condition; do
statement(s)
done

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for dir in ${dirs_to_cleanup}; do
if [[ -d "${dir}/${ORACLE_SID}" ]]; then
log_date "Cleaning up old files in ${dir}/${ORACLE_SID}"
rm "${dir}/${ORACLE_SID}/"*
if [[ "$?" -ne 0 ]]; then
error_message
fi
else
mkdir -p "${dir}/${ORACLE_SID}"
if [[ "$?" -ne 0 ]]; then
error_message
fi
fi
done

case语句

  • 通过2个空格缩进可选项。
  • 在同一行可选项的模式右圆括号之后和结束符 ;;之前各需要一个空格。
  • 长可选项或者多命令可选项应该被拆分成多行,模式、操作和结束符;;在不同的行。

匹配表达式比caseesac 缩进一级。多行操作要再缩进一级。一般情况下,不需要引用匹配表达式。模式表达式前面不应该出现左括号。避免使用;&;;&符号。即:

1
2
3
4
5
6
7
8
9
10
11
12
13
# case structure
case in expression in
pattern1)
statement1
;;
pattern2)
statement2
;;
...
*)
statementn
;;
esac

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
case "${expression}" in
a)
variable="..."
some_command "${variable}" "${other_expr}" ...
;;
absolute)
actions="relative"
another_command "${actions}" "${other_expr}" ...
;;
*)
error "Unexpected expression '${expression}'"
;;
esac

只要整个表达式可读,简单的命令可以跟模式和;; 写在同一行。这通常适用于单字母选项的处理。当单行容不下操作时,请将模式单独放一行,然后是操作,最后结束符;; 也单独一行。当操作在同一行时,模式的右括号之后和结束符;;之前请使用一个空格分隔。

1
2
3
4
5
6
7
8
9
10
11
12
13
verbose='false'
aflag=''
bflag=''
files=''
while getopts 'abf:v' flag; do
case "${flag}" in
a) aflag='true' ;;
b) bflag='true' ;;
f) files="${OPTARG}" ;;
v) verbose='true' ;;
*) error "Unexpected option ${flag}" ;;
esac
done

变量扩展

按优先级顺序:保持跟你所发现的一致;引用你的变量;推荐用${var}而不是$var

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Section of recommended cases.

# Preferred style for 'special' variables:
echo "Positional: $1" "$5" "$3"
echo "Specials: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ \$=$$ ..."

# Braces necessary:
echo "many parameters: ${10}"

# Braces avoiding confusion:
# Output is "a0b0c0"
set -- a b c
echo "${1}0${2}0${3}0"

# Preferred style for other variables:
echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}"
while read f; do
echo "file=${f}"
done < <(ls -l /tmp)

# Section of discouraged cases

# Unquoted vars, unbraced vars, brace-quoted single letter
# shell specials.
echo a=$avar "b=$bvar" "PID=${$}" "${1}"

# Confusing use: this is expanded as "${1}0${2}0${3}0",
# not "${10}${20}${30}
set -- a b c
echo "$10$20$30"

特性

命令替换

使用 $(command)而不是反引号。

嵌套的反引号要求用反斜杠转义内部的反引号。而$(command) 形式嵌套时不需要改变,而且更易于阅读。

例如:

1
2
3
4
5
# This is preferred:
var="$(command "$(command1)")"

# This is not:
var="`command \`command1\``"

文件名的通配符扩展

当进行文件名的通配符扩展时,请使用明确的路径。

因为文件名可能以-开头,所以使用扩展通配符./**来得安全得多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Here's the contents of the directory:
# -f -r somedir somefile

# This deletes almost everything in the directory by force
psa@bilby$ rm -v *
removed directory: `somedir'
removed `somefile'

# As opposed to:
psa@bilby$ rm -v ./*
removed `./-f'
removed `./-r'
rm: cannot remove `./somedir': Is a directory
removed `./somefile'

命名约定

函数名

使用小写字母,并用下划线分隔单词。使用双冒号 :: 分隔库。函数名之后必须有圆括号。关键词 function 是可选的,但必须在一个项目中保持一致。

如果你正在写单个函数,请用小写字母来命名,并用下划线分隔单词。如果你正在写一个包,使用双冒号 :: 来分隔包名。大括号必须和函数名位于同一行(就像在Google的其他语言一样),并且函数名和圆括号之间没有空格。

1
2
3
4
5
6
7
8
9
# Single function
my_func() {
...
}

# Part of a package
mypackage::my_func() {
...
}

当函数名后存在 () 时,关键词 function 是多余的。但是其促进了函数的快速辨识。

变量名

使用小写字母,循环的变量名应该和循环的任何变量同样命名。例如:

1
2
3
for zone in ${zones}; do
something_with "${zone}"
done

常量和环境变量名

全部使用大写字母,用下划线分隔,声明在文件的顶部。例如:

1
2
3
4
5
# Constant
readonly PATH_TO_FILES='/some/path'

# Both constant and environment
declare -xr ORACLE_SID='PROD'

源文件名

使用小写字母,如果需要的话使用下划线分隔单词。例如: maketemplate 或者 make_template ,而不是 make-template

只读变量

使用小写字母,使用 readonly 或者 declare -r 来确保变量只读。

因为全局变量在Shell中广泛使用,所以在使用它们的过程中捕获错误是很重要的。当你声明了一个变量,希望其只读,那么请明确指出。

1
2
3
4
5
6
zip_version="$(dpkg --status zip | grep Version: | cut -d ' ' -f 2)"
if [[ -z "${zip_version}" ]]; then
error_message
else
readonly zip_version
fi

使用本地变量

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

使用 local 来声明局部变量以确保其只在函数内部和子函数中可见。这避免了污染全局命名空间和不经意间设置可能具有函数之外重要性的变量。

当赋值的值由命令替换提供时,声明和赋值必须分开。因为内建的 local 不会从命令替换中传递退出码。

1
2
3
4
5
6
7
8
9
10
11
12
13
my_func2() {
local name="$1"

# Separate lines for declaration and assignment:
local my_var
my_var="$(my_func)" || return

# DO NOT do this: $? contains the exit code of 'local', not my_func
local my_var="$(my_func)"
[[ $? -eq 0 ]] || return

...
}

调用命令

检查返回值

对于非管道命令,使用$?或直接通过一个if语句来检查以保持其简洁。例如:

1
2
3
4
5
6
7
8
9
10
11
if ! mv "${file_list}" "${dest_dir}/" ; then
echo "Unable to move ${file_list} to ${dest_dir}" >&2
exit "${E_BAD_MOVE}"
fi

# Or
mv "${file_list}" "${dest_dir}/"
if [[ "$?" -ne 0 ]]; then
echo "Unable to move ${file_list} to ${dest_dir}" >&2
exit "${E_BAD_MOVE}"
fi

Bash也有 PIPESTATUS 变量,允许检查从管道所有部分返回的代码。如果仅仅需要检查整个管道是成功还是失败,以下的方法是可以接受的:

1
2
3
4
tar -cf - ./* | ( cd "${dir}" && tar -xf - )
if [[ "${PIPESTATUS[0]}" -ne 0 || "${PIPESTATUS[1]}" -ne 0 ]]; then
echo "Unable to tar files to ${dir}" >&2
fi

可是,只要你运行任何其他命令, PIPESTATUS 将会被覆盖。如果你需要基于管道中发生的错误执行不同的操作,那么你需要在运行命令后立即将 PIPESTATUS 赋值给另一个变量(别忘了 [ 是一个会将 PIPESTATUS 擦除的命令)。

1
2
3
4
5
6
7
8
tar -cf - ./* | ( cd "${DIR}" && tar -xf - )
return_codes=(${PIPESTATUS[*]})
if [[ "${return_codes[0]}" -ne 0 ]]; then
do_something
fi
if [[ "${return_codes[1]}" -ne 0 ]]; then
do_something_else
fi