1. Shell编程入门概述
Shell脚本是Linux/Unix系统管理员和开发人员的必备技能。它允许我们通过编写简单的文本文件来自动化重复性任务,将复杂的命令行操作封装成可重复执行的程序。与编译型语言不同,Shell脚本是解释执行的,这使得它特别适合系统管理、文件操作和快速原型开发。
我在十多年的运维工作中,Shell脚本帮我完成了90%的日常系统管理工作。从简单的日志清理到复杂的部署流程,一个精心编写的脚本可以节省大量时间。对于初学者来说,Shell脚本的优势在于:
- 无需复杂开发环境,任何文本编辑器都能编写
- 直接调用系统命令,学习曲线平缓
- 即时测试即时生效,调试周期短
2. 基础语法精要
2.1 脚本结构解析
每个Shell脚本都应该以shebang开头:
#!/bin/bash # 这是注释 echo "Hello World"第一行指定解释器路径,#开头的行是注释。保存为hello.sh后,需要添加执行权限:
chmod +x hello.sh ./hello.sh2.2 变量操作技巧
变量定义和使用时注意:
name="value" # 等号两边不能有空格 echo $name # 使用变量要加$ echo ${name} # 推荐这种写法,明确变量边界重要细节:
- 变量默认都是字符串类型
- 局部变量用local声明(函数内)
- 使用
readonly声明常量 - 特殊变量:
$0(脚本名)$1(第一个参数)$#(参数个数)
3. 流程控制实战
3.1 条件判断进阶
if语句的三种形式:
# 基本形式 if [ condition ]; then commands fi # 带else if [ -f "/path/file" ]; then echo "文件存在" else echo "文件不存在" fi # 多条件 if [[ $var -gt 10 ]]; then echo "大于10" elif [[ $var -lt 5 ]]; then echo "小于5" else echo "5到10之间" fi测试文件常用选项:
-e文件存在-f是普通文件-d是目录-r可读-w可写-x可执行
3.2 循环结构优化
for循环的几种写法:
# 传统C风格 for ((i=0; i<10; i++)); do echo $i done # 遍历列表 for color in red green blue; do echo $color done # 遍历命令输出 for file in $(ls *.txt); do echo "处理文件: $file" donewhile循环示例:
count=1 while [ $count -le 5 ]; do echo "计数: $count" ((count++)) done4. 函数与错误处理
4.1 函数编写规范
定义和使用函数:
function log_message { local timestamp=$(date "+%Y-%m-%d %H:%M:%S") echo "[$timestamp] $1" >> /var/log/myscript.log } log_message "脚本开始执行"最佳实践:
- 函数名使用小写加下划线
- 用local声明局部变量
- 通过
$1$2获取参数 $@表示所有参数return只能返回数字(0-255)
4.2 错误处理机制
# 检查上条命令是否成功 if ! mkdir /tmp/mydir; then echo "创建目录失败" >&2 exit 1 fi # 设置错误退出 set -euo pipefail # -e: 命令失败立即退出 # -u: 使用未定义变量报错 # -o pipefail: 管道中任意命令失败则整个管道失败 # 捕获信号 trap 'cleanup; exit' SIGINT SIGTERM5. 文本处理三剑客
5.1 grep高效搜索
# 基本搜索 grep "pattern" file.txt # 常用选项 grep -i "error" /var/log/syslog # 忽略大小写 grep -v "debug" app.log # 反向匹配 grep -r "function" /src # 递归搜索 grep -A3 -B2 "exception" log.txt # 显示匹配前后行5.2 sed流编辑器
# 替换文本 sed 's/old/new/g' input.txt # 直接修改文件 sed -i.bak 's/foo/bar/' file.txt # 删除行 sed '/pattern/d' file.txt # 多重操作 sed -e 's/a/A/g' -e 's/b/B/g' file.txt5.3 awk数据处理
# 打印特定列 awk '{print $1,$3}' data.txt # 条件过滤 awk '$3 > 100 {print $0}' sales.csv # 使用分隔符 awk -F: '{print $1,$6}' /etc/passwd # 计算统计 awk '{sum+=$3} END {print sum}' data.txt6. 实用脚本案例
6.1 日志分析脚本
#!/bin/bash # 分析Nginx日志获取访问量前10的IP LOG_FILE="/var/log/nginx/access.log" OUTPUT_FILE="/tmp/top_ips.txt" if [ ! -f "$LOG_FILE" ]; then echo "错误: 日志文件不存在" >&2 exit 1 fi echo "Top 10访问IP:" > $OUTPUT_FILE awk '{print $1}' $LOG_FILE | sort | uniq -c | sort -nr | head -10 >> $OUTPUT_FILE echo "分析完成,结果保存在 $OUTPUT_FILE"6.2 系统监控脚本
#!/bin/bash # 监控系统资源使用情况 ALERT_THRESHOLD=90 EMAIL="admin@example.com" check_disk() { local usage=$(df -h | awk '$NF=="/"{print $5}' | tr -d '%') if [ $usage -gt $ALERT_THRESHOLD ]; then echo "磁盘使用率过高: $usage%" | mail -s "磁盘警报" $EMAIL fi } check_memory() { local free=$(free -m | awk '/Mem/{printf "%.0f", $3/$2*100}') if [ $free -gt $ALERT_THRESHOLD ]; then echo "内存使用率过高: $free%" | mail -s "内存警报" $EMAIL fi } check_disk check_memory7. 调试与优化技巧
7.1 脚本调试方法
# 打印执行命令 set -x # 你的脚本代码 set +x # 另一种方式 bash -x script.sh # 检查语法而不执行 bash -n script.sh # 使用trap调试 trap 'echo "在行 $LINENO: var=$var"' DEBUG7.2 性能优化建议
- 减少子进程创建:
# 不好: 每次循环都调用date for i in {1..100}; do echo $(date) done # 更好: 只调用一次date now=$(date) for i in {1..100}; do echo $now done- 使用内置字符串操作代替外部命令:
# 不好 filename=$(echo $fullpath | awk -F/ '{print $NF}') # 更好 filename=${fullpath##*/}- 大量文本处理时,考虑使用awk代替多个grep/sed组合
8. 安全注意事项
8.1 安全编程实践
- 总是验证用户输入:
read -p "输入目录: " dir if [ ! -d "$dir" ]; then echo "错误: 目录不存在" >&2 exit 1 fi- 避免使用eval:
# 危险! eval $user_input # 应该使用数组安全执行 cmd=(ls -l "$dir") "${cmd[@]}"- 设置安全的默认权限:
umask 077 # 创建的文件默认600,目录7008.2 常见陷阱
- 未引用的变量:
# 如果文件名包含空格会出问题 for file in $(ls); do rm $file done # 正确做法 for file in *; do rm "$file" done- 管道中的错误被忽略:
# 即使failcmd失败,echo也会执行 failcmd | echo "继续执行" # 正确做法 set -o pipefail failcmd | echo "现在会捕获错误"- 整数比较错误:
# 字符串比较 if [ "10" \< "2" ]; then echo "10比2小?" fi # 正确整数比较 if (( 10 < 2 )); then echo "这不会执行" fi9. 进阶技巧
9.1 数组和关联数组
# 普通数组 colors=("red" "green" "blue") echo ${colors[1]} # 输出green colors+=(yellow) # 添加元素 # 关联数组(Bash 4+) declare -A user user[name]="Alice" user[age]=25 echo ${user[name]}9.2 进程替换
# 比较两个命令的输出 diff <(ls dir1) <(ls dir2) # 避免创建临时文件 paste <(cut -f1 file1) <(cut -f3 file2)9.3 并行处理
# 使用&后台执行 for url in "${urls[@]}"; do wget "$url" & done wait # 等待所有后台任务完成 # 使用xargs并行 printf "%s\n" "${files[@]}" | xargs -P4 -I{} process_file {}10. 跨平台兼容性
10.1 避免bashism
如果脚本需要跨平台运行,避免使用Bash特有特性:
# 使用POSIX test而不是[[ ]] if [ "$var" -gt 10 ]; then # 使用printf代替echo -e printf "Line1\nLine2\n" # 避免使用数组,改用空格分隔字符串 files="file1 file2 file3" for file in $files; do [ -f "$file" ] || continue # 处理文件 done10.2 检测运行环境
#!/bin/sh # 检测Bash版本 if [ -n "$BASH_VERSION" ]; then echo "运行在Bash $BASH_VERSION" else echo "警告: 建议使用Bash执行此脚本" >&2 fi # 检查工具是否存在 check_command() { if ! command -v "$1" >/dev/null 2>&1; then echo "错误: $1 命令未找到" >&2 exit 1 fi } check_command awk check_command sed11. 代码组织与维护
11.1 模块化设计
将常用功能分离到单独文件:
# lib/utils.sh log_info() { echo "[INFO] $(date '+%F %T') - $*" } # main.sh source "$(dirname "$0")/lib/utils.sh" log_info "脚本启动"11.2 文档规范
在脚本头部添加标准文档:
#!/bin/bash # 名称: 系统备份脚本 # 描述: 自动备份指定目录到远程服务器 # 作者: Your Name # 版本: 1.0 # 用法: ./backup.sh [选项] <目录> # 选项: # -c 启用压缩 # -v 详细输出11.3 版本控制
建议将脚本纳入Git管理:
# .gitignore *.tmp *.log12. 实战项目:自动化部署脚本
#!/bin/bash set -euo pipefail # 配置 APP_NAME="myapp" APP_DIR="/opt/$APP_NAME" BACKUP_DIR="/var/backups/$APP_NAME" REPO_URL="git@example.com:myrepo.git" BRANCH="production" # 初始化目录 init_dirs() { mkdir -p "$BACKUP_DIR" mkdir -p "$APP_DIR" } # 备份当前版本 backup_current() { local timestamp=$(date '+%Y%m%d%H%M%S') tar -czf "$BACKUP_DIR/$timestamp.tar.gz" -C "$APP_DIR" . } # 部署新版本 deploy() { cd "$APP_DIR" if [ -d .git ]; then git fetch origin git checkout "$BRANCH" git reset --hard "origin/$BRANCH" else git clone "$REPO_URL" . git checkout "$BRANCH" fi } # 重启服务 restart_service() { systemctl restart "${APP_NAME}.service" } # 主流程 main() { echo "开始部署 $APP_NAME" init_dirs backup_current deploy restart_service echo "部署成功完成" } main "$@"