08 – Shell 脚本编程入门
一、Shell 脚本基础
1.1 第一个脚本
#!/bin/bash
# 这是注释
echo "Hello, World!"
echo "当前用户:$(whoami)"
echo "当前时间:$(date '+%Y-%m-%d %H:%M:%S')"
- #!/bin/bash 称为 shebang,指定解释器
- # 开头为注释
- echo 输出内容
- $() 执行命令并获取输出
1.2 执行脚本
# 方法一:添加执行权限后执行
chmod +x script.sh
./script.sh
# 方法二:用解释器执行(不需要执行权限)
bash script.sh
# 方法三:source 执行(在当前 Shell 中执行,变量不释放)
source script.sh
. script.sh # 等同于 source
1.3 脚本规范
#!/bin/bash
#==================================================================
# 脚本名称:backup.sh
# 功能描述:备份指定目录
# 作者:运维团队
# 创建日期:2024-01-15
# 使用方法:./backup.sh <源目录> <目标目录>
#==================================================================
set -e # 遇到错误即退出
set -u # 使用未定义变量报错
set -o pipefail # 管道中任一命令失败即失败
# 或合并写:set -euo pipefail
二、变量
2.1 变量定义与使用
# 变量定义(等号两边不能有空格!)
name="alice"
age=25
PI=3.14159
# 使用变量
echo $name
echo "我的名字是 ${name}" # 推荐加花括号
# 命令替换
current_date=$(date +%Y%m%d) # 推荐写法
current_date=`date +%Y%m%d` # 反引号写法(不推荐)
# 只读变量
readonly HOSTNAME="web-01"
# 删除变量
unset name
2.2 特殊变量
| $0 | 脚本名称 |
| $1~$9 | 第 1~9 个参数 |
| ${10} | 第 10 个及以后的参数 |
| $# | 参数个数 |
| $* | 所有参数(作为单个字符串) |
| $@ | 所有参数(每个参数独立) |
| $? | 上一条命令的退出状态码 |
| $$ | 当前脚本的 PID |
| $! | 最近一个后台进程的 PID |
#!/bin/bash
echo "脚本名称:$0"
echo "参数个数:$#"
echo "第一个参数:$1"
echo "所有参数:$@"
echo "上一条命令状态:$?"
2.3 字符串操作
str="Hello World"
# 字符串长度
echo ${#str} # 11
# 子字符串(从位置 0 开始,取 5 个字符)
echo ${str:0:5} # Hello
# 字符串替换
file="backup.tar.gz"
echo ${file%.gz} # 去掉后缀:backup.tar
echo ${file%%.*} # 去掉最长匹配:backup
echo ${file#backup.} # 去掉前缀:tar.gz
echo ${file##*.} # 取后缀:gz
echo ${file/backup/restore} # 替换第一个匹配:restore.tar.gz
echo ${file//a/A} # 替换所有匹配:bAckup.tAr.gz
2.4 默认值
# 如果 var 为空,使用默认值(不赋值)
echo ${var:-"default"}
# 如果 var 为空,赋值并使用默认值
echo ${var:="default"}
# 如果 var 为空,报错退出
echo ${var😕"var is not set"}
三、条件判断
3.1 if 语句
# 基本格式
if [ $age -ge 18 ]; then
echo "成年人"
elif [ $age -ge 13 ]; then
echo "青少年"
else
echo "儿童"
fi
3.2 条件测试
# 数值比较
[ $a -eq $b ] # 等于
[ $a -ne $b ] # 不等于
[ $a -gt $b ] # 大于
[ $a -lt $b ] # 小于
[ $a -ge $b ] # 大于等于
[ $a -le $b ] # 小于等于
# 字符串比较
[ "$str1" = "$str2" ] # 相等
[ "$str1" != "$str2" ] # 不相等
[ -z "$str" ] # 为空
[ -n "$str" ] # 非空
# 文件测试
[ -f file ] # 存在且是普通文件
[ -d dir ] # 存在且是目录
[ -e path ] # 存在
[ -r file ] # 可读
[ -w file ] # 可写
[ -x file ] # 可执行
[ -s file ] # 存在且非空
[ file1 -nt file2 ] # file1 比 file2 新
[ file1 -ot file2 ] # file1 比 file2 旧
# 逻辑运算
[ $a -gt 0 ] && [ $a -lt 100 ] # 与
[ $a -lt 0 ] || [ $a -gt 100 ] # 或
[ ! -f file ] # 非
# 推荐使用 [[ ]](bash 扩展,支持通配符和正则)
[[ $str == h* ]] # 通配符匹配
[[ $str =~ ^[0-9]+$ ]] # 正则匹配
3.3 case 语句
#!/bin/bash
read -p "请输入服务名: " service
case $service in
nginx|httpd)
echo "Web 服务器"
systemctl status $service
;;
mysql|mariadb)
echo "数据库服务"
systemctl status mysqld
;;
*)
echo "未知服务: $service"
echo "可选: nginx, httpd, mysql, mariadb"
;;
esac
四、循环
4.1 for 循环
# 列表遍历
for i in 1 2 3 4 5; do
echo "数字: $i"
done
# 范围遍历
for i in {1..10}; do
echo "数字: $i"
done
for i in {1..10..2}; do # 步长为 2
echo "数字: $i"
done
# 遍历命令输出
for file in /etc/*.conf; do
echo "配置文件: $file"
done
# C 语言风格
for ((i=0; i<10; i++)); do
echo "索引: $i"
done
# 遍历数组
servers=("web01" "web02" "db01")
for server in "${servers[@]}"; do
echo "服务器: $server"
done
4.2 while 循环
# 基本循环
count=0
while [ $count -lt 5 ]; do
echo "计数: $count"
((count++))
done
# 读取文件
while read line; do
echo "行内容: $line"
done < /etc/passwd
# 无限循环
while true; do
echo "运行中…"
sleep 5
done
4.3 循环控制
# break – 跳出循环
for i in {1..10}; do
if [ $i -eq 5 ]; then
break
fi
echo $i
done
# continue – 跳过本次
for i in {1..10}; do
if [ $((i % 2)) -eq 0 ]; then
continue
fi
echo "奇数: $i"
done
五、函数
# 定义函数
function greet() {
local name=$1 # local 变量仅在函数内有效
echo "你好, $name!"
return 0 # 返回状态码(0-255)
}
# 调用函数
greet "alice"
# 函数返回值通过命令替换获取
get_date() {
date +%Y%m%d
}
today=$(get_date)
echo "今天是 $today"
实用函数示例
# 日志函数
log_info() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $1"
}
log_error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $1" >&2
}
log_info "开始部署"
log_error "配置文件不存在"
# 检查命令是否存在
check_command() {
if ! command -v $1 &> /dev/null; then
echo "错误: $1 未安装"
exit 1
fi
}
check_command docker
check_command curl
六、数组
# 定义数组
fruits=("apple" "banana" "cherry")
nums=(1 2 3 4 5)
# 从命令输出创建数组
files=($(ls /etc/*.conf))
# 访问元素
echo ${fruits[0]} # apple
echo ${fruits[@]} # 所有元素
echo ${#fruits[@]} # 数组长度
# 遍历
for fruit in "${fruits[@]}"; do
echo $fruit
done
# 添加元素
fruits+=("orange")
# 删除元素
unset fruits[1] # 删除索引 1 的元素
七、输入输出
7.1 用户输入
# 读取输入
read -p "请输入用户名: " username
echo "你好, $username"
# 读取密码(不显示输入)
read -s -p "请输入密码: " password
echo ""
echo "密码长度: ${#password}"
# 读取多个值
read -p "输入姓名和年龄: " name age
# 限时读取
read -t 10 -p "10秒内输入: " input
7.2 重定向
# 输出重定向
command > file # 覆盖写入
command >> file # 追加写入
# 错误重定向
command 2> error.log # 只重定向错误
command > all.log 2>&1 # 标准输出和错误都重定向
command &> all.log # 同上(bash 简写)
# 输入重定向
command < input.txt
# /dev/null 黑洞
command > /dev/null 2>&1 # 丢弃所有输出
# Here Document
cat << EOF
这是多行文本
变量会被展开: $HOME
EOF
# Here String
grep "pattern" <<< "搜索这段文本"
八、实战脚本
脚本一:自动备份
#!/bin/bash
#==================================================================
# 脚本名称:auto_backup.sh
# 功能:备份指定目录,保留最近 7 天的备份
#==================================================================
set -euo pipefail
SOURCE_DIR="/data/app"
BACKUP_DIR="/backup"
KEEP_DAYS=7
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/app_backup_${DATE}.tar.gz"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# 检查源目录
if [ ! -d "$SOURCE_DIR" ]; then
log "错误:源目录 $SOURCE_DIR 不存在"
exit 1
fi
# 创建备份目录
mkdir -p "$BACKUP_DIR"
# 执行备份
log "开始备份 $SOURCE_DIR …"
tar -czf "$BACKUP_FILE" -C "$(dirname $SOURCE_DIR)" "$(basename $SOURCE_DIR)"
if [ $? -eq 0 ]; then
log "备份成功:$BACKUP_FILE ($(du -sh $BACKUP_FILE | awk '{print $1}'))"
else
log "备份失败!"
exit 1
fi
# 清理旧备份
log "清理 ${KEEP_DAYS} 天前的旧备份…"
find "$BACKUP_DIR" -name "app_backup_*.tar.gz" -mtime +$KEEP_DAYS -delete
log "备份完成"
脚本二:批量检查服务器状态
#!/bin/bash
#==================================================================
# 脚本名称:check_servers.sh
# 功能:批量检查多台服务器的连通性和负载
#==================================================================
SERVERS=("192.168.1.10" "192.168.1.11" "192.168.1.12")
PORT=22
TIMEOUT=3
echo "=========================================="
echo " 服务器状态检查 – $(date '+%Y-%m-%d %H:%M')"
echo "=========================================="
printf "%-16s %-8s %-12s %-10s\\n" "IP地址" "状态" "负载" "响应时间"
echo "——————————————"
for server in "${SERVERS[@]}"; do
# 检查端口
if nc -z -w $TIMEOUT $server $PORT 2>/dev/null; then
status="在线"
# 获取负载(通过 SSH)
load=$(ssh -o ConnectTimeout=$TIMEOUT $server "cat /proc/loadavg | awk '{print \\$1}'" 2>/dev/null)
load=${load:-"N/A"}
# ping 响应时间
rtime=$(ping -c 1 -W $TIMEOUT $server 2>/dev/null | grep 'time=' | awk -F'time=' '{print $2}' | awk '{print $1}')
rtime=${rtime:-"N/A"}"ms"
else
status="离线"
load="-"
rtime="-"
fi
printf "%-16s %-8s %-12s %-10s\\n" "$server" "$status" "$load" "$rtime"
done
echo "=========================================="
脚本三:服务部署脚本
#!/bin/bash
#==================================================================
# 脚本名称:deploy.sh
# 功能:部署 Web 应用
#==================================================================
set -euo pipefail
APP_NAME="webapp"
APP_DIR="/opt/$APP_NAME"
APP_USER="webapp"
PORT=3000
VERSION="${1:-latest}"
log() { echo "[$(date '+%H:%M:%S')] $1"; }
# 前置检查
for cmd in node npm; do
command -v $cmd >/dev/null 2>&1 || { echo "错误: $cmd 未安装"; exit 1; }
done
# 停止旧服务
if systemctl is-active –quiet $APP_NAME; then
log "停止旧服务…"
systemctl stop $APP_NAME
fi
# 备份当前版本
if [ -d "$APP_DIR" ]; then
log "备份当前版本…"
mv "$APP_DIR" "${APP_DIR}_backup_$(date +%Y%m%d_%H%M%S)"
fi
# 创建目录
mkdir -p "$APP_DIR"
cd "$APP_DIR"
# 拉取代码(此处用 git 示例)
log "拉取代码 (版本: $VERSION)…"
# git clone –branch $VERSION https://github.com/example/webapp.git .
# 安装依赖
log "安装依赖…"
npm install –production
# 构建项目
log "构建项目…"
npm run build
# 配置 systemd 服务
log "配置系统服务…"
cat > /etc/systemd/system/${APP_NAME}.service << EOF
[Unit]
Description=$APP_NAME
After=network.target
[Service]
Type=simple
User=$APP_USER
WorkingDirectory=$APP_DIR
ExecStart=/usr/bin/node $APP_DIR/server.js
Restart=on-failure
Environment=PORT=$PORT
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable $APP_NAME
systemctl start $APP_NAME
# 验证
sleep 2
if systemctl is-active –quiet $APP_NAME; then
log "部署成功!服务运行在端口 $PORT"
systemctl status $APP_NAME –no-pager
else
log "部署失败!"
journalctl -u $APP_NAME -n 20 –no-pager
exit 1
fi
九、调试技巧
# 方法一:bash -x 显示执行的每条命令
bash -x script.sh
# 方法二:脚本内开启调试
set -x # 开启调试
set +x # 关闭调试
# 方法三:只调试部分代码
set -x
# … 需要调试的代码 …
set +x
# 方法四:语法检查(不执行)
bash -n script.sh
十、小结
本篇介绍了 Shell 脚本编程的核心知识:变量、条件判断、循环、函数、数组、输入输出重定向以及实战脚本示例。Shell 脚本是运维自动化的基础工具,掌握后可以大幅提升工作效率。编写脚本时注意遵循 set -euo pipefail 规范,让脚本更健壮。
上一篇:07 – 磁盘管理与 LVM 下一篇:09 – 定时任务与自动化运维



