开头指定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 errexitcp 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_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" EXITfunction 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_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 errexitset -o nounsetreadonly WORKSPACE_DIR="$(cd "$(dirname "${BASH_SOUCRCE[0]} " ) " ) " && pwd )" readonly THIS_FILE=" ${WORKSPACE_DIR} "/$(basename ${BASH_SOUCRCE[0]}")" readonly ROOT_DIR="${WORKSPACE_DIR} " /../" #your code