Shell 脚本
问题
请详细介绍 Shell 脚本编程,包括变量、流程控制、函数和文本处理三剑客。
答案
Shell 基础
Shell 脚本是运维自动化的基础工具。Bash 是最常用的 Shell。
hello.sh
#!/bin/bash
# Shebang 行指定解释器
set -euo pipefail # 严格模式(推荐)
# -e: 命令失败立即退出
# -u: 使用未定义变量报错
# -o pipefail: 管道中任一命令失败则整体失败
echo "Hello, DevOps!"
变量与数据类型
# 变量定义(等号两边不能有空格)
name="DevOps"
port=8080
readonly VERSION="1.0.0" # 只读变量
# 使用变量
echo "$name"
echo "${name}_engineer" # 花括号界定变量名
# 命令替换
today=$(date +%Y-%m-%d)
files=$(ls /tmp | wc -l)
# 算术运算
count=$((count + 1))
result=$((10 * 5 / 2))
# 字符串操作
str="Hello World"
echo ${#str} # 长度: 11
echo ${str:0:5} # 截取: Hello
echo ${str/World/Shell} # 替换: Hello Shell
echo ${str,,} # 转小写: hello world
echo ${str^^} # 转大写: HELLO WORLD
# 默认值
echo ${VAR:-"default"} # VAR 未定义时返回 default
echo ${VAR:="default"} # VAR 未定义时设置并返回 default
echo ${VAR:?"error msg"} # VAR 未定义时报错退出
# 数组
servers=("web01" "web02" "web03")
echo ${servers[0]} # 第一个元素
echo ${servers[@]} # 所有元素
echo ${#servers[@]} # 数组长度
servers+=("web04") # 追加元素
# 关联数组(Bash 4+)
declare -A config
config[host]="192.168.1.100"
config[port]="8080"
echo ${config[host]}
特殊变量
| 变量 | 含义 |
|---|---|
$0 | 脚本名称 |
$1~$9 | 位置参数 |
$# | 参数个数 |
$@ | 所有参数(独立字符串) |
$* | 所有参数(单个字符串) |
$? | 上个命令的退出码 |
$$ | 当前脚本的 PID |
$! | 最后一个后台进程的 PID |
$_ | 上个命令的最后一个参数 |
流程控制
# if-else
if [[ -f "/etc/nginx/nginx.conf" ]]; then
echo "Nginx config exists"
elif [[ -f "/etc/httpd/httpd.conf" ]]; then
echo "Apache config exists"
else
echo "No web server config found"
fi
# 常用判断条件
# 文件测试
[[ -f file ]] # 文件存在且是普通文件
[[ -d dir ]] # 目录存在
[[ -e path ]] # 路径存在
[[ -r file ]] # 可读
[[ -w file ]] # 可写
[[ -x file ]] # 可执行
[[ -s file ]] # 文件非空
# 字符串比较
[[ "$str" == "hello" ]] # 相等
[[ "$str" != "hello" ]] # 不等
[[ -z "$str" ]] # 为空
[[ -n "$str" ]] # 非空
[[ "$str" =~ ^[0-9]+$ ]] # 正则匹配
# 数字比较
[[ $a -eq $b ]] # 等于
[[ $a -ne $b ]] # 不等于
[[ $a -gt $b ]] # 大于
[[ $a -lt $b ]] # 小于
[[ $a -ge $b ]] # 大于等于
[[ $a -le $b ]] # 小于等于
# for 循环
for server in web01 web02 web03; do
echo "Deploying to $server"
ssh "$server" "cd /opt/app && git pull"
done
# C 风格 for
for ((i=1; i<=10; i++)); do
echo "Iteration $i"
done
# 遍历文件
for file in /var/log/*.log; do
echo "Processing: $file"
gzip "$file"
done
# while 循环
count=0
while [[ $count -lt 5 ]]; do
echo "Count: $count"
((count++))
done
# 读取文件每一行
while IFS= read -r line; do
echo "Line: $line"
done < /etc/hosts
# case 语句
case "$1" in
start)
echo "Starting service..."
;;
stop)
echo "Stopping service..."
;;
restart)
echo "Restarting service..."
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac
函数
# 函数定义
log_info() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"
}
log_error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2
}
# 带返回值的函数
check_service() {
local service_name="$1"
if systemctl is-active --quiet "$service_name"; then
return 0 # 成功
else
return 1 # 失败
fi
}
# 函数调用
log_info "Deployment started"
if check_service nginx; then
log_info "Nginx is running"
else
log_error "Nginx is not running"
fi
# 捕获函数输出
get_ip() {
hostname -I | awk '{print $1}'
}
local_ip=$(get_ip)
echo "Local IP: $local_ip"
文本处理三剑客
grep - 文本搜索
# 基础搜索
grep "error" /var/log/syslog
grep -i "error" log.txt # 忽略大小写
grep -r "TODO" /opt/app/ # 递归搜索
grep -n "error" log.txt # 显示行号
grep -c "error" log.txt # 统计匹配行数
grep -v "debug" log.txt # 反向匹配(排除)
grep -l "error" /var/log/*.log # 只显示文件名
# 正则表达式
grep -E "error|warning|critical" log.txt # 扩展正则
grep -P "\d{4}-\d{2}-\d{2}" log.txt # Perl 正则
# 上下文
grep -A 3 "error" log.txt # 显示匹配行后 3 行(After)
grep -B 3 "error" log.txt # 显示匹配行前 3 行(Before)
grep -C 3 "error" log.txt # 显示匹配行前后各 3 行(Context)
# 实战: 查找最近 1 小时内的错误日志
grep "$(date -d '1 hour ago' '+%Y-%m-%d %H')" /var/log/app.log | grep -i error
sed - 流编辑器
# 替换
sed 's/old/new/' file.txt # 替换每行第一个匹配
sed 's/old/new/g' file.txt # 替换所有匹配
sed -i 's/old/new/g' file.txt # 直接修改文件(-i 就地编辑)
sed -i.bak 's/old/new/g' file.txt # 修改前备份
# 删除
sed '/^#/d' file.txt # 删除注释行
sed '/^$/d' file.txt # 删除空行
sed '1,5d' file.txt # 删除前 5 行
# 插入/追加
sed '2i\new line' file.txt # 在第 2 行前插入
sed '2a\new line' file.txt # 在第 2 行后追加
# 打印
sed -n '10,20p' file.txt # 打印第 10-20 行
sed -n '/error/p' file.txt # 打印匹配行
# 实战: 批量修改配置
sed -i 's/listen 80/listen 8080/g' /etc/nginx/conf.d/*.conf
sed -i 's/worker_processes auto/worker_processes 4/' /etc/nginx/nginx.conf
awk - 文本分析
# 基础
awk '{print $1, $3}' file.txt # 打印第 1 和第 3 列
awk -F: '{print $1, $3}' /etc/passwd # 指定分隔符为 :
awk -F',' '{print NR, $1, $2}' data.csv # CSV 解析
# 条件过滤
awk '$3 > 90 {print $1, $3}' scores.txt # 第 3 列大于 90
awk '/error/ {print}' log.txt # 匹配 error 的行
awk -F: '$3 >= 1000 {print $1}' /etc/passwd # UID >= 1000 的用户
# 内置变量
# NR: 当前行号 NF: 当前行的字段数 FS: 字段分隔符 OFS: 输出分隔符
# 统计
awk '{sum += $1} END {print "Total:", sum}' numbers.txt
awk '{sum += $1; count++} END {print "Avg:", sum/count}' numbers.txt
# 实战: 分析 Nginx access log
# 统计各状态码数量
awk '{print $9}' access.log | sort | uniq -c | sort -rn
# 统计 Top 10 IP
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
# 统计每秒请求数(QPS)
awk '{print $4}' access.log | cut -d: -f1-3 | sort | uniq -c | sort -rn | head -10
# 找出响应时间超过 1 秒的请求
awk '$NF > 1.0 {print $7, $NF}' access.log | sort -k2 -rn | head -20
实用脚本模板
部署脚本
deploy.sh
#!/bin/bash
set -euo pipefail
# 配置
APP_NAME="myapp"
DEPLOY_DIR="/opt/$APP_NAME"
BACKUP_DIR="/opt/backups/$APP_NAME"
GIT_REPO="git@github.com:org/myapp.git"
BRANCH="${1:-main}"
# 日志函数
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
# 备份当前版本
backup() {
local timestamp=$(date +%Y%m%d_%H%M%S)
log "Backing up to $BACKUP_DIR/$timestamp"
mkdir -p "$BACKUP_DIR"
cp -r "$DEPLOY_DIR" "$BACKUP_DIR/$timestamp"
# 只保留最近 5 个备份
ls -dt "$BACKUP_DIR"/*/ | tail -n +6 | xargs rm -rf
}
# 部署
deploy() {
log "Deploying $BRANCH to $DEPLOY_DIR"
cd "$DEPLOY_DIR"
git fetch origin
git checkout "$BRANCH"
git pull origin "$BRANCH"
# 安装依赖、构建等
# npm install && npm run build
}
# 重启服务
restart_service() {
log "Restarting $APP_NAME"
systemctl restart "$APP_NAME"
sleep 3
if systemctl is-active --quiet "$APP_NAME"; then
log "Service $APP_NAME is running"
else
log "ERROR: Service $APP_NAME failed to start"
exit 1
fi
}
# 主流程
main() {
log "=== Deployment started ==="
backup
deploy
restart_service
log "=== Deployment completed ==="
}
main "$@"
健康检查脚本
health_check.sh
#!/bin/bash
set -euo pipefail
WEBHOOK_URL="${ALERT_WEBHOOK:-}"
HOSTNAME=$(hostname)
check_disk() {
# 检查磁盘使用率是否超过 85%
df -h | awk 'NR>1 && +$5 > 85 {printf "DISK WARNING: %s usage %s\n", $6, $5}'
}
check_memory() {
local mem_usage=$(free | awk '/^Mem:/ {printf "%.0f", $3/$2*100}')
if [[ $mem_usage -gt 90 ]]; then
echo "MEMORY WARNING: ${mem_usage}% used"
fi
}
check_cpu() {
local cpu_idle=$(top -bn1 | grep "Cpu(s)" | awk '{print $8}')
local cpu_usage=$(echo "100 - $cpu_idle" | bc)
if (( $(echo "$cpu_usage > 90" | bc -l) )); then
echo "CPU WARNING: ${cpu_usage}% used"
fi
}
check_services() {
local services=("nginx" "redis" "mysql")
for svc in "${services[@]}"; do
if ! systemctl is-active --quiet "$svc" 2>/dev/null; then
echo "SERVICE DOWN: $svc"
fi
done
}
# 收集所有告警
alerts=""
alerts+=$(check_disk)
alerts+=$(check_memory)
alerts+=$(check_cpu)
alerts+=$(check_services)
if [[ -n "$alerts" ]]; then
echo "[$HOSTNAME] Health check alerts:"
echo "$alerts"
# 发送告警(如钉钉、Slack)
if [[ -n "$WEBHOOK_URL" ]]; then
curl -s -X POST "$WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "{\"text\": \"[$HOSTNAME] $alerts\"}"
fi
fi
常见面试问题
Q1: set -euo pipefail 是什么?为什么推荐使用?
答案:
这是 Bash 的"严格模式":
-e:任何命令失败(非零退出码)立即退出脚本-u:引用未定义变量时报错退出(而非空值)-o pipefail:管道中任一命令失败,整个管道返回非零
推荐在所有生产脚本中使用,避免错误被忽略导致后续操作在错误状态上继续执行。
Q2: $@ 和 $* 的区别?
答案:
不加引号时两者相同。加双引号时:
"$@"展开为多个独立字符串:"$1" "$2" "$3""$*"展开为一个完整字符串:"$1 $2 $3"
遍历参数时应使用 "$@",保持参数中的空格不被拆分。
Q3: 如何用 awk 统计 Nginx 日志中 Top 10 访问 IP?
答案:
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
原理:awk 提取第一列(IP),sort 排序使相同 IP 相邻,uniq -c 去重并计数,sort -rn 按数量降序排列,head -10 取前 10。
Q4: 单引号和双引号的区别?
答案:
- 双引号
"":变量会被展开,命令替换会执行 - 单引号
'':所有内容原样输出,不做任何展开
name="World"
echo "Hello $name" # Hello World
echo 'Hello $name' # Hello $name
Q5: 如何实现脚本的互斥执行(防止重复运行)?
答案:
LOCKFILE="/tmp/myscript.lock"
# 方法: flock 文件锁
exec 9>"$LOCKFILE"
if ! flock -n 9; then
echo "Another instance is running"
exit 1
fi
# 脚本退出时自动释放锁(文件描述符关闭)
trap 'rm -f "$LOCKFILE"' EXIT
# 主逻辑...