news 2026/2/28 22:23:18

Shell脚本编程最佳实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Shell脚本编程最佳实践

前言

写Shell脚本容易,写好Shell脚本难。随手写的脚本能跑,但换个环境就出问题;脚本越写越长,自己都看不懂;没有错误处理,跑到一半失败了也不知道。

本文整理Shell脚本编程的最佳实践,从代码规范到错误处理,让脚本更健壮、更易维护。


1. 脚本基础规范

1.1 标准模板

#!/bin/bash## 脚本名称: deploy.sh# 功能描述: 自动化部署脚本# 作者: your_name# 创建时间: 2025-01-08# 使用方式: ./deploy.sh [env] [version]#set-euo pipefail# 全局变量readonlySCRIPT_DIR="$(cd "$(dirname"${BASH_SOURCE[0]}")"&&pwd)" readonly SCRIPT_NAME="$(basename"$0")" readonly LOG_FILE="/var/log/${SCRIPT_NAME%.sh}.log" # 默认值 ENV="${1:-prod}" VERSION="${2:-latest}" # 主函数 main() { log "开始执行..." # 主逻辑 log "执行完成" } # 日志函数 log() { echo "[$(date'+%Y-%m-%d %H:%M:%S')]$*" | tee -a "$LOG_FILE" } # 执行 main "$@"

1.2 set命令详解

set-e# 遇到错误立即退出set-u# 使用未定义变量报错set-o pipefail# 管道中任一命令失败则整体失败set-x# 调试模式,打印每条命令# 组合使用set-euo pipefail

为什么需要这些设置

# 没有 set -e 的问题rm-rf /important/data# 假设这里写错了路径echo"删除成功"# 即使上面失败,这行还会执行# 没有 set -u 的问题echo$UNDEFIND_VAR# 不报错,只是空值rm-rf$UNDEFIND_VAR/*# 危险!可能变成 rm -rf /*# 没有 set -o pipefail 的问题cat/nonexistent|grep"test"|wc-lecho$?# 返回0,因为wc成功了,但cat其实失败了

1.3 变量使用规范

# 使用大括号包裹变量echo"${name}"# 推荐echo"$name"# 可以,但不够清晰# 给变量设置默认值name="${1:-default}"# 如果$1为空,使用defaultname="${1:=default}"# 如果$1为空,赋值并使用defaultname="${1:?错误信息}"# 如果$1为空,报错退出# 字符串操作file="/path/to/file.txt"echo"${file%.txt}"# /path/to/file 去掉后缀echo"${file##*/}"# file.txt 只取文件名echo"${file%/*}"# /path/to 只取目录# 只读变量readonlyCONFIG_FILE="/etc/app.conf"# 局部变量(在函数中)localtemp_var="value"

2. 函数编写

2.1 函数定义规范

# 推荐写法function_name(){localarg1="$1"localarg2="${2:-default}"# 函数逻辑return0}# 带返回值的函数get_memory_usage(){localusageusage=$(free|awk'/Mem:/ {printf "%.1f", ($2-$7)/$2*100}')echo"$usage"}# 调用mem=$(get_memory_usage)echo"内存使用率:${mem}%"

2.2 参数处理

#!/bin/bash# 处理命令行参数usage(){cat<<EOF Usage:$0[OPTIONS] Options: -e, --env ENV 环境 (dev|test|prod) -v, --version VER 版本号 -f, --force 强制执行 -h, --help 显示帮助 EOFexit1}# 默认值ENV=""VERSION=""FORCE=false# 解析参数while[[$#-gt0]];docase"$1"in-e|--env)ENV="$2"shift2;;-v|--version)VERSION="$2"shift2;;-f|--force)FORCE=trueshift;;-h|--help)usage;;*)echo"未知参数:$1"usage;;esacdone# 参数检查if[[-z"$ENV"]];thenecho"错误: 必须指定环境"usagefi

2.3 返回值与退出码

# 使用返回值check_service(){ifsystemctl is-active --quiet"$1";thenreturn0# 成功elsereturn1# 失败fi}ifcheck_service nginx;thenecho"nginx运行中"elseecho"nginx未运行"fi# 自定义退出码readonlyEXIT_SUCCESS=0readonlyEXIT_INVALID_ARGS=1readonlyEXIT_FILE_NOT_FOUND=2readonlyEXIT_PERMISSION_DENIED=3[[-f"$config_file"]]||exit$EXIT_FILE_NOT_FOUND

3. 错误处理

3.1 trap捕获信号

#!/bin/bashset-euo pipefail# 临时文件TEMP_FILE=$(mktemp)# 清理函数cleanup(){localexit_code=$?rm-f"$TEMP_FILE"echo"清理完成,退出码:$exit_code"exit$exit_code}# 捕获退出信号trapcleanup EXITtrap'echo "收到中断信号"; exit 130'INTTERM# 主逻辑echo"正在处理..."# ... 脚本逻辑 ...

3.2 错误处理函数

#!/bin/bash# 错误处理error_handler(){localline_no=$1localerror_code=$2echo"错误发生在第$line_no行,退出码:$error_code"# 可以在这里发送告警}trap'error_handler ${LINENO} $?'ERR# 日志函数log_error(){echo"[ERROR]$(date'+%Y-%m-%d %H:%M:%S')$*">&2}log_info(){echo"[INFO]$(date'+%Y-%m-%d %H:%M:%S')$*"}log_warn(){echo"[WARN]$(date'+%Y-%m-%d %H:%M:%S')$*"}

3.3 重试机制

# 带重试的函数retry(){localmax_attempts=$1localdelay=$2shift2localcmd="$@"localattempt=1while[[$attempt-le$max_attempts]];doecho"尝试第$attempt次:$cmd"ifeval"$cmd";thenreturn0fiif[[$attempt-lt$max_attempts]];thenecho"失败,${delay}秒后重试..."sleep"$delay"fi((attempt++))doneecho"达到最大重试次数,失败"return1}# 使用retry35curl-f http://example.com/health

4. 常用技巧

4.1 安全的文件操作

# 安全删除(先检查)safe_rm(){localtarget="$1"# 防止误删根目录if[["$target"=="/"]]||[[-z"$target"]];thenecho"危险操作,拒绝执行"return1fi# 检查是否存在if[[!-e"$target"]];thenecho"目标不存在:$target"return1firm-rf"$target"}# 安全的目录切换cd_safe(){cd"$1"||{echo"无法进入目录:$1"exit1}}# 创建目录(如果不存在)mkdir-p"$target_dir"

4.2 并行执行

#!/bin/bash# 并行处理# 方法1:后台进程forserverinserver1 server2 server3;dossh"$server"'uptime'&donewait# 等待所有后台进程完成# 方法2:xargs并行echo"server1 server2 server3"|tr' ''\n'|\xargs-P3-I{}ssh{}'uptime'# 方法3:GNU parallel(需安装)parallel -j4ssh{}uptime::: server1 server2 server3 server4

4.3 锁机制防止重复执行

#!/bin/bash# 使用flock防止脚本重复执行LOCK_FILE="/tmp/$(basename"$0").lock"exec200>"$LOCK_FILE"if!flock -n200;thenecho"脚本已在运行中"exit1fi# 主逻辑echo"开始执行..."sleep60echo"执行完成"

4.4 配置文件读取

#!/bin/bash# 读取配置文件CONFIG_FILE="${1:-/etc/app.conf}"# 检查文件存在[[-f"$CONFIG_FILE"]]||{echo"配置文件不存在:$CONFIG_FILE"exit1}# 方法1:source(注意安全风险)# source "$CONFIG_FILE"# 方法2:逐行解析(更安全)whileIFS='='read-r key value;do# 跳过注释和空行[["$key"=~^#.*$ ]] && continue[[-z"$key"]]&&continue# 去掉空格key=$(echo"$key"|xargs)value=$(echo"$value"|xargs)# 赋值declare"$key=$value"done<"$CONFIG_FILE"echo"DB_HOST:$DB_HOST"echo"DB_PORT:$DB_PORT"

4.5 颜色输出

#!/bin/bash# 颜色定义RED='\033[0;31m'GREEN='\033[0;32m'YELLOW='\033[0;33m'BLUE='\033[0;34m'NC='\033[0m'# No Colorecho_red(){echo-e"${RED}$*${NC}";}echo_green(){echo-e"${GREEN}$*${NC}";}echo_yellow(){echo-e"${YELLOW}$*${NC}";}echo_blue(){echo-e"${BLUE}$*${NC}";}# 使用echo_green"[OK] 服务正常"echo_red"[FAIL] 服务异常"echo_yellow"[WARN] 磁盘空间不足"

5. 调试技巧

5.1 调试模式

#!/bin/bash# 调试开关DEBUG=${DEBUG:-false}debug(){if[["$DEBUG"=true]];thenecho"[DEBUG]$*">&2fi}# 使用debug"变量值: name=$name"# 执行时开启调试# DEBUG=true ./script.sh

5.2 详细执行跟踪

#!/bin/bash# 在需要调试的代码段前后开关set-x# 开启# ... 需要调试的代码 ...set+x# 关闭# 或者指定调试输出位置exec5>/tmp/debug.logBASH_XTRACEFD=5set-x

5.3 shellcheck静态检查

# 安装# yum install shellcheck 或 apt install shellcheck# 检查脚本shellcheckscript.sh# 常见问题示例# SC2086: Double quote to prevent globbing and word splitting# SC2046: Quote this to prevent word splitting# SC2034: Variable appears unused

6. 实战示例

6.1 服务部署脚本

#!/bin/bash## deploy.sh - 服务部署脚本#set-euo pipefailreadonlySCRIPT_DIR="$(cd "$(dirname"${BASH_SOURCE[0]}")"&&pwd)" readonly APP_NAME="myapp" readonly DEPLOY_DIR="/opt/${APP_NAME}" readonly BACKUP_DIR="/opt/backup/${APP_NAME}" # 颜色 RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' log_info() { echo -e "${GREEN}[INFO]${NC}$*"; } log_error() { echo -e "${RED}[ERROR]${NC}$*" >&2; } usage() { cat <<EOF Usage:$0<version> Example:$0v1.2.3 EOF exit 1 } # 参数检查 VERSION="${1:-}" [[ -z "$VERSION" ]] && usage # 检查环境 check_env() { log_info "检查环境..." # 检查权限 [[$EUID-eq 0 ]] || { log_error "需要root权限" exit 1 } # 检查目录 mkdir -p "$DEPLOY_DIR" "$BACKUP_DIR" } # 备份 backup() { log_info "备份当前版本..." if [[ -d "$DEPLOY_DIR/current" ]]; then local backup_name="${APP_NAME}_$(date+%Y%m%d_%H%M%S)" cp -r "$DEPLOY_DIR/current" "$BACKUP_DIR/$backup_name" log_info "备份到:$BACKUP_DIR/$backup_name" fi } # 下载 download() { log_info "下载版本:$VERSION" local download_url="https://releases.example.com/${APP_NAME}/${VERSION}.tar.gz" local temp_file=$(mktemp)curl -fsSL "$download_url" -o "$temp_file" || { log_error "下载失败" rm -f "$temp_file" exit 1 } # 解压 mkdir -p "$DEPLOY_DIR/$VERSION" tar -xzf "$temp_file" -C "$DEPLOY_DIR/$VERSION" rm -f "$temp_file" } # 切换版本 switch_version() { log_info "切换到新版本..." # 更新软链接 ln -sfn "$DEPLOY_DIR/$VERSION" "$DEPLOY_DIR/current" } # 重启服务 restart_service() { log_info "重启服务..." systemctl restart "$APP_NAME" || { log_error "重启失败,尝试回滚" rollback exit 1 } # 健康检查 sleep 5 if ! curl -sf http://localhost:8080/health > /dev/null; then log_error "健康检查失败,尝试回滚" rollback exit 1 fi } # 回滚 rollback() { log_info "回滚到上一版本..." local latest_backup=$(ls-t"$BACKUP_DIR"|head-1)if [[ -n "$latest_backup" ]]; then ln -sfn "$BACKUP_DIR/$latest_backup" "$DEPLOY_DIR/current" systemctl restart "$APP_NAME" fi } # 清理旧版本 cleanup() { log_info "清理旧版本..." # 保留最近5个备份 cd "$BACKUP_DIR" && ls -t | tail -n +6 | xargs -r rm -rf } # 主函数 main() { check_env backup download switch_version restart_service cleanup log_info "部署完成:$VERSION"}main

6.2 批量服务器执行脚本

#!/bin/bash## batch_exec.sh - 批量服务器执行命令#set-euo pipefail# 服务器列表(可以从文件读取)SERVERS=("10.10.0.1""10.10.0.2""10.10.0.3")# SSH选项SSH_OPTS="-o ConnectTimeout=5 -o StrictHostKeyChecking=no"usage(){cat<<EOF Usage:$0<command> Example:$0"uptime"$0"df -h"$0"systemctl status nginx" EOFexit1}COMMAND="${1:-}"[[-z"$COMMAND"]]&&usage# 执行forserverin"${SERVERS[@]}";doecho"======$server======"ssh$SSH_OPTS"$server""$COMMAND"2>&1||echo"执行失败"echo""done

如果服务器分布在不同网络,可以先用组网工具(WireGuard、ZeroTier、星空组网等)把机器串起来,脚本里直接用虚拟IP,不用关心实际网络环境。


7. 常见问题

7.1 空格和特殊字符

# 错误:变量没加引号forfilein$files;do# 如果文件名有空格会出问题# 正确:加引号forfilein"$files";do# 处理文件名有空格的情况whileIFS=read-r -d''file;doecho"处理:$file"done<<(find.-name"*.txt"-print0)

7.2 数组操作

# 定义数组arr=("item1""item2""item3")# 遍历foritemin"${arr[@]}";doecho"$item"done# 数组长度echo"长度:${#arr[@]}"# 追加元素arr+=("item4")# 索引访问echo"第一个:${arr[0]}"

7.3 字符串比较

# 字符串比较用 [[ ]]if[["$str1"=="$str2"]];thenecho"相等"fi# 正则匹配if[["$str"=~^[0-9]+$]];thenecho"是数字"fi# 数值比较用 (( )) 或 -eqif((num1>num2));thenecho"num1更大"fiif[[$num1-gt$num2]];thenecho"num1更大"fi

总结

类别最佳实践
基础set -euo pipefail,变量加引号
变量使用${}包裹,设置默认值
函数使用local声明局部变量
错误处理trap捕获信号,清理临时文件
日志带时间戳,区分级别
调试使用shellcheckset -x
安全防止误删,检查参数合法性

写Shell脚本的原则:

  1. 健壮性:考虑各种异常情况
  2. 可读性:清晰的命名和注释
  3. 可维护性:模块化,避免重复
  4. 安全性:防止误操作,谨慎使用rm

更多运维技术文章,欢迎关注公众号:北平的秋葵

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/16 6:03:47

教育行业AI应用:用M2FP开发动作评估系统的实战路径

教育行业AI应用&#xff1a;用M2FP开发动作评估系统的实战路径 在教育智能化转型的浪潮中&#xff0c;人工智能正从“辅助教学”向“深度参与教学过程”演进。尤其是在体育、舞蹈、康复训练等强调身体动作规范性与协调性的教学场景中&#xff0c;如何实现对学生动作的客观化、可…

作者头像 李华
网站建设 2026/2/11 5:28:06

企业选型参考:M2FP与其他商业人体解析API的成本效益对比

企业选型参考&#xff1a;M2FP与其他商业人体解析API的成本效益对比 在数字化内容生产、智能安防、虚拟试衣和人机交互等场景中&#xff0c;人体解析&#xff08;Human Parsing&#xff09; 技术正成为关键基础设施。它不仅要求识别“人在哪里”&#xff0c;更需精确到“身体各…

作者头像 李华
网站建设 2026/2/26 16:25:41

Z-Image-Turbo影视分镜草图生成潜力挖掘

Z-Image-Turbo影视分镜草图生成潜力挖掘 引言&#xff1a;AI图像生成在影视前期的破局点 在影视创作流程中&#xff0c;分镜设计是连接剧本与实拍的关键环节。传统方式依赖美术师手绘或使用专业软件逐帧构图&#xff0c;耗时长、成本高&#xff0c;且难以快速迭代。随着AIGC技…

作者头像 李华
网站建设 2026/2/26 14:55:25

【Java毕设源码分享】基于springboot+vue的农用车4S店管理系统的设计与实现(程序+文档+代码讲解+一条龙定制)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华