深色模式
Bash 进阶
摘要:会写命令不等于会写 Shell。本文覆盖写好运维脚本最常用的四块:函数与返回值、数组、参数解析(getopts)、以及
set -euo pipefail与trap这类容易踩坑的语言特性。
适用环境
bash
bash --version | head -1 # 建议 4.x 及以上
which shellcheck || echo "建议安装 shellcheck 做静态检查"1
2
2
操作步骤
一、严格模式:先理解再开启
bash
set -e # 任意命令返回非零即退出(注意:在 if/while 条件、&& || 右侧不生效)
set -u # 使用未定义变量即报错(避免 rm -rf $DIR/ 这种灾难)
set -o pipefail # 管道中任一环节失败则整体失败
set -euo pipefail1
2
3
4
2
3
4
注意
set -e 不会在函数作为 if 条件、|| 右侧时生效。常见误区是以为开了 -e 就万事大吉,实际仍需对关键命令显式检查返回值。
二、函数:返回值与输出要分开
bash
#!/usr/bin/env bash
set -euo pipefail
# 用输出返回数据,用 return 返回状态码
get_disk_usage() {
local path="${1:?需要传入路径}"
df -P "$path" | awk 'NR==2 {gsub(/%/,"",$5); print $5}'
}
check_disk() {
local path="$1" limit="$2"
local usage
usage="$(get_disk_usage "$path")"
if (( usage > limit )); then
echo "WARN $path 使用率 ${usage}% > ${limit}%"
return 1
fi
echo "OK $path 使用率 ${usage}%"
return 0
}
check_disk / 80 || echo "磁盘告警已触发"1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
变量一律用 local 声明,避免污染全局;${1:?} 在参数缺失时直接报错退出。
三、数组:批量处理的基础
bash
# 定义
hosts=(web01 web02 web03)
ports=(80 443 8080)
# 遍历
for h in "${hosts[@]}"; do
echo "checking $h"
done
# 长度与下标
echo "共 ${#hosts[@]} 台"
echo "第一台 ${hosts[0]}"
# 追加
hosts+=("web04")
# 关联数组(Bash 4+)
declare -A conf
conf[timeout]=30
conf[retries]=3
echo "${conf[timeout]}"
# 从命令输出构建数组(注意用 mapfile 而非反引号)
mapfile -t files < <(find /var/log -name '*.log' -mtime +7)
printf '%s\n' "${files[@]}"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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
四、参数解析:getopts
bash
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<EOF
用法: $(basename "$0") -h <host> [-p port] [-t timeout] [-d]
-h 目标主机(必填)
-p 端口,默认 22
-t 超时秒数,默认 5
-d 调试模式
EOF
exit 2
}
host="" port=22 timeout=5 debug=0
while getopts ":h:p:t:d" opt; do
case "$opt" in
h) host="$OPTARG" ;;
p) port="$OPTARG" ;;
t) timeout="$OPTARG" ;;
d) debug=1 ;;
:) echo "选项 -$OPTARG 缺少参数" >&2; usage ;;
\?) echo "未知选项 -$OPTARG" >&2; usage ;;
esac
done
[[ -z "$host" ]] && usage
(( debug )) && set -x # 调试模式打印每条命令
echo "host=$host port=$port timeout=$timeout"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
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
五、trap:收尾与信号
bash
TMPDIR_RUN="$(mktemp -d)"
cleanup() {
local rc=$?
rm -rf "$TMPDIR_RUN"
echo "退出码 $rc"
}
trap cleanup EXIT # 正常/异常退出都会执行
trap 'echo "收到中断,退出"; exit 130' INT TERM1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
六、字符串与算术
bash
name="/var/log/app.log"
echo "${name##*/}" # app.log 去最长前缀
echo "${name%.log}" # /var/log/app 去后缀
echo "${name/app/error}" # 替换
echo "${name^^}" # 转大写
# 算术用 (( )),不要 expr
(( count++ ))
total=$(( a + b ))
# 浮点用 bc 或 awk
echo "scale=2; 10/3" | bc
awk 'BEGIN{printf "%.2f\n", 10/3}'1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
七、退出码约定
text
0 成功
1 通用失败
2 用法错误(参数不对)
3 前置条件不满足
4 超时1
2
3
4
5
2
3
4
5
验证
- [ ] 函数用
local声明变量,用return返回状态码 - [ ] 数组遍历全部使用
"${arr[@]}"带引号形式 - [ ]
getopts能正确处理缺参数与未知选项 - [ ]
trap cleanup EXIT在Ctrl+C时也会执行清理 - [ ]
shellcheck无 error 级告警
常见坑
$?被覆盖:echo "xxx"之后$?变成 echo 的返回值,要立即保存rc=$?。set -e与函数返回值冲突:f() { grep x y; }在grep失败时直接退出脚本,需用|| true或if包裹。- 数组写成
${arr[*]}:元素含空格时会被合并/拆分,必须用${arr[@]}且带引号。 - 用反引号嵌套命令:可读性差且转义复杂,一律改用
$(...)。 - 局部变量漏
local:函数修改全局变量导致后续逻辑莫名出错。