2026/5/21 9:26:16
网站建设
项目流程
商务网站的主要存在形式,濮阳专业做网站公司,晋中推广型网站建设,网站建设讨论会前言
写Shell脚本容易#xff0c;写好Shell脚本难。随手写的脚本能跑#xff0c;但换个环境就出问题#xff1b;脚本越写越长#xff0c;自己都看不懂#xff1b;没有错误处理#xff0c;跑到一半失败了也不知道。
本文整理Shell脚本编程的最佳实践#xff0c;从代码规范…前言写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|greptest|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.txtecho${file%.txt}# /path/to/file 去掉后缀echo${file##*/}# file.txt 只取文件名echo${file%/*}# /path/to 只取目录# 只读变量readonlyCONFIG_FILE/etc/app.conf# 局部变量在函数中localtemp_varvalue2. 函数编写2.1 函数定义规范# 推荐写法function_name(){localarg1$1localarg2${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(){catEOF Usage:$0[OPTIONS] Options: -e, --env ENV 环境 (dev|test|prod) -v, --version VER 版本号 -f, --force 强制执行 -h, --help 显示帮助 EOFexit1}# 默认值ENVVERSIONFORCEfalse# 解析参数while[[$#-gt0]];docase$1in-e|--env)ENV$2shift2;;-v|--version)VERSION$2shift2;;-f|--force)FORCEtrueshift;;-h|--help)usage;;*)echo未知参数:$1usage;;esacdone# 参数检查if[[-z$ENV]];thenecho错误: 必须指定环境usagefi2.3 返回值与退出码# 使用返回值check_service(){ifsystemctl is-active --quiet$1;thenreturn0# 成功elsereturn1# 失败fi}ifcheck_service nginx;thenechonginx运行中elseechonginx未运行fi# 自定义退出码readonlyEXIT_SUCCESS0readonlyEXIT_INVALID_ARGS1readonlyEXIT_FILE_NOT_FOUND2readonlyEXIT_PERMISSION_DENIED3[[-f$config_file]]||exit$EXIT_FILE_NOT_FOUND3. 错误处理3.1 trap捕获信号#!/bin/bashset-euo pipefail# 临时文件TEMP_FILE$(mktemp)# 清理函数cleanup(){localexit_code$?rm-f$TEMP_FILEecho清理完成退出码:$exit_codeexit$exit_code}# 捕获退出信号trapcleanup EXITtrapecho 收到中断信号; exit 130INTTERM# 主逻辑echo正在处理...# ... 脚本逻辑 ...3.2 错误处理函数#!/bin/bash# 错误处理error_handler(){localline_no$1localerror_code$2echo错误发生在第$line_no行退出码:$error_code# 可以在这里发送告警}traperror_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$localattempt1while[[$attempt-le$max_attempts]];doecho尝试第$attempt次:$cmdifeval$cmd;thenreturn0fiif[[$attempt-lt$max_attempts]];thenecho失败${delay}秒后重试...sleep$delayfi((attempt))doneecho达到最大重试次数失败return1}# 使用retry35curl-f http://example.com/health4. 常用技巧4.1 安全的文件操作# 安全删除先检查safe_rm(){localtarget$1# 防止误删根目录if[[$target/]]||[[-z$target]];thenecho危险操作拒绝执行return1fi# 检查是否存在if[[!-e$target]];thenecho目标不存在:$targetreturn1firm-rf$target}# 安全的目录切换cd_safe(){cd$1||{echo无法进入目录:$1exit1}}# 创建目录如果不存在mkdir-p$target_dir4.2 并行执行#!/bin/bash# 并行处理# 方法1后台进程forserverinserver1 server2 server3;dossh$serveruptimedonewait# 等待所有后台进程完成# 方法2xargs并行echoserver1 server2 server3|tr \n|\xargs-P3-I{}ssh{}uptime# 方法3GNU parallel需安装parallel -j4ssh{}uptime::: server1 server2 server3 server44.3 锁机制防止重复执行#!/bin/bash# 使用flock防止脚本重复执行LOCK_FILE/tmp/$(basename$0).lockexec200$LOCK_FILEif!flock -n200;thenecho脚本已在运行中exit1fi# 主逻辑echo开始执行...sleep60echo执行完成4.4 配置文件读取#!/bin/bash# 读取配置文件CONFIG_FILE${1:-/etc/app.conf}# 检查文件存在[[-f$CONFIG_FILE]]||{echo配置文件不存在:$CONFIG_FILEexit1}# 方法1source注意安全风险# source $CONFIG_FILE# 方法2逐行解析更安全whileIFSread-r key value;do# 跳过注释和空行[[$key~^#.*$ ]] continue[[-z$key]]continue# 去掉空格key$(echo$key|xargs)value$(echo$value|xargs)# 赋值declare$key$valuedone$CONFIG_FILEechoDB_HOST:$DB_HOSTechoDB_PORT:$DB_PORT4.5 颜色输出#!/bin/bash# 颜色定义RED\033[0;31mGREEN\033[0;32mYELLOW\033[0;33mBLUE\033[0;34mNC\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[[$DEBUGtrue]];thenecho[DEBUG]$*2fi}# 使用debug变量值: name$name# 执行时开启调试# DEBUGtrue ./script.sh5.2 详细执行跟踪#!/bin/bash# 在需要调试的代码段前后开关set-x# 开启# ... 需要调试的代码 ...setx# 关闭# 或者指定调试输出位置exec5/tmp/debug.logBASH_XTRACEFD5set-x5.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 unused6. 实战示例6.1 服务部署脚本#!/bin/bash## deploy.sh - 服务部署脚本#set-euo pipefailreadonlySCRIPT_DIR$(cd $(dirname${BASH_SOURCE[0]})pwd) readonly APP_NAMEmyapp 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:$0version 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_urlhttps://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}main6.2 批量服务器执行脚本#!/bin/bash## batch_exec.sh - 批量服务器执行命令#set-euo pipefail# 服务器列表可以从文件读取SERVERS(10.10.0.110.10.0.210.10.0.3)# SSH选项SSH_OPTS-o ConnectTimeout5 -o StrictHostKeyCheckingnousage(){catEOF Usage:$0command Example:$0uptime$0df -h$0systemctl status nginx EOFexit1}COMMAND${1:-}[[-z$COMMAND]]usage# 执行forserverin${SERVERS[]};doecho$serverssh$SSH_OPTS$server$COMMAND21||echo执行失败echodone如果服务器分布在不同网络可以先用组网工具WireGuard、ZeroTier、星空组网等把机器串起来脚本里直接用虚拟IP不用关心实际网络环境。7. 常见问题7.1 空格和特殊字符# 错误变量没加引号forfilein$files;do# 如果文件名有空格会出问题# 正确加引号forfilein$files;do# 处理文件名有空格的情况whileIFSread-r -dfile;doecho处理:$filedone(find.-name*.txt-print0)7.2 数组操作# 定义数组arr(item1item2item3)# 遍历foritemin${arr[]};doecho$itemdone# 数组长度echo长度:${#arr[]}# 追加元素arr(item4)# 索引访问echo第一个:${arr[0]}7.3 字符串比较# 字符串比较用 [[ ]]if[[$str1$str2]];thenecho相等fi# 正则匹配if[[$str~^[0-9]$]];thenecho是数字fi# 数值比较用 (( )) 或 -eqif((num1num2));thenechonum1更大fiif[[$num1-gt$num2]];thenechonum1更大fi总结类别最佳实践基础set -euo pipefail变量加引号变量使用${}包裹设置默认值函数使用local声明局部变量错误处理trap捕获信号清理临时文件日志带时间戳区分级别调试使用shellcheckset -x安全防止误删检查参数合法性写Shell脚本的原则健壮性考虑各种异常情况可读性清晰的命名和注释可维护性模块化避免重复安全性防止误操作谨慎使用rm更多运维技术文章欢迎关注公众号北平的秋葵