开头指定bash

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

1
2
#!/usr/bin/env bash
#!/bin/bash

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

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

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

用双引号包围变量

如以下片段

1
2
3
4
5
#!/bin/bash
filename="hah hah"
if [$filename = "test"];then
echo "test"
fi

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

1
2
3
4
5
#!/bin/bash
filename="hah hah"
if ["$filename" = "test"];then
echo "test"
fi

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

全部代码进函数

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

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
function func1(){
echo "func1"
}
function main(){
echo "$@"
cp a.txt b.txt
rm -f a.txt
func1
}
main "$@"

使用readonly定义常量

1
2
3
#!/bin/bash
readonly WORKSPACE_DIR="/data"
echo "$WORKSPACE_DIR"

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

关注变量作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
WORKSPACE_DIR="/data/"
echo "$WORKSPACE_DIR"
function func1()
{
local WORKSPACE_DIR="/home/"
local a="haha"
b="xixi"
echo "$WORKSPACE_DIR"
}
function main()
{
func1
echo "$WORKSPACE_DIR"
}
main "$@"
echo "$a"
echo "$b"

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

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

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

警惕未被初始化的变量

1
2
3
#!/bin/bash
path=$1
echo "tyr to rm $path"

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

1
2
3
4
#!/bin/bash
set -o unset
path=$1
echo "tyr to rm $path"

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

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

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

让代码执行可追踪

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

防止错误滚雪球

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

1
2
3
#!/bin/bash
cp a.txt b.txt
rm -f a.txt

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

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

1
2
3
4
5
6
7
#!/bin/bash
cp a.txt b.txt
if [$? -ne 0 ];then
exit 1
fi
rm -f a.txt
echo "删除成功"

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

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

1
2
3
4
5
#!/bin/bash
set -o errexit
cp a.txt b.txt
rm -f a.txt
echo "删除成功"

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

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

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

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

学会查路径

1
2
3
4
5
#!/bin/bash
readonly WORKSPACE_DIR="$(cd "$(dirname "${BASH_SOUCRCE[0]}")")" && pwd)"
readonly THIS_FILE="${WORKSPACE_DIR}"/$(basename ${BASH_SOUCRCE[0]}")"
readonly BIN_DIR="${WORKSPACE_DIR}"/../bin"
readonly EXECUTE_PATH=$(pwd)

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

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

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

巧用shift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
function func1(){
local arg1="$1"
local arg2="$2"
echo "$arg1" "$arg2"
}
function func2(){
local arg1=$1 && shift
local arg2="$1" && shift
echo "$arg1" "$arg2"
}
function main( ){
func1 "$@"
func2 "$@"
}
main "$@"

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

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
31
32
33
34
35
36
#!/bin/bash
function main_eval_param()
{
local eval_param=""
while [[ $# -gt 0 ]];do
key="$1"
case $key in
--eal | -e)
# eval使用其后的所有参数作为eval目标的参数
eval_param="${@:2}"
break
;;
--file | -f)
UPLOAD_FILE_NAME="$2" && shift
;;
--module | -m)
MODULE_NAME="$2" && shift
;;
* | --help | -h)
_help
exit 1
;;
esac
shift
done
return 0
}

function main()
{
main_eval_param "$@"
echo ${UPOLOAD_FILE_NAME}
echo ${MODULE_NAME}
}

main "$@"

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

封装一些常用指令

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

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
function check_job()
{
local ret=$?
local msg="$1" && shift
if [[ $ret -ne 0 ]];then
error "[${ret}:${msg}]"
exit 1
fi
return 0
}

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

提供help信息

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

切换目录的几种方式

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env bash
readonly WORKSPACE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]]")"&& pwd)"
#第一种写法,使用pushd和popd
pushd $WORKSPACE_DIR >/dev/null
# do some work
popd > /dev/null

#第二种写法
cd $WORKSPACE_DIR#
do some workcd
cd -

# 第三种写法,用小括号括起来,使用子shell
(
cd $WORKSPACE_DIR
# do some work
)

巧用trap信号

1
2
3
4
5
6
7
#!/bin/bash

trap "handle_exit_code" EXIT
function handle_exit_code()
{
echo "handle_exit_code"
}

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

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

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
31
32
33
34
35
#!/bin/bash
function simple_eval_param()
{
local eval_param=""
while [[ $# -gt 0 ]];do
key="$1"
case $key in
--eal | -e)
# eval使用其后的所有参数作为eval目标的参数
eval_param="${@:2}"
break
;;
* | --help | -h)
_help
exit 1
;;
esac
shift
done
if [[ "${eval_param}" != "" ]];then
eval "${eval_param}"
exit $?
fi
return 0
}

function start(){
echo "start"
}

function main()
{
simple_eval_param "$@"
}
main "$@"

如上编写的脚本(假设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 “$@” 的形式。以下是各种形式传参的结果:

Syntax Effective result
$* $1 $2 $3 … ${N}
$@ $1 $2 $3 … ${N}
“$*” “$1c$2c$3c…c${N}”
“$@” “$1” “$2” “$3” … “${N}”

脚本可以这样开始

1
2
3
4
5
6
7
8
9
#!/bin/bash

set -o errexit
set -o nounset
readonly WORKSPACE_DIR="$(cd "$(dirname "${BASH_SOUCRCE[0]}")")" && pwd)"
readonly THIS_FILE="${WORKSPACE_DIR}"/$(basename ${BASH_SOUCRCE[0]}")"
readonly ROOT_DIR="${WORKSPACE_DIR}"/../"

#your code