Appearance
Shell 脚本编程
本章将介绍 Shell 脚本编程,包括脚本基础、变量、流程控制、函数、输入输出以及实战案例,帮助你编写自动化脚本。
脚本基础
第一个脚本
bash
#!/bin/bash
# 这是第一个 Shell 脚本
# 文件名:hello.sh
# 输出欢迎信息
echo "Hello, World!"
echo "当前用户:$USER"
echo "当前目录:$(pwd)"
# 运行脚本的方式:
# 1. 添加执行权限后运行
chmod +x hello.sh
./hello.sh
# 2. 使用解释器运行
bash hello.sh
# 3. 使用 source 执行(在当前 Shell 中执行)
source hello.sh
. hello.sh脚本结构
bash
#!/bin/bash
# =====================================
# 脚本名称:script_name.sh
# 功能描述:脚本功能说明
# 作 者:作者名
# 创建日期:2024-01-15
# 修改日期:2024-01-16
# =====================================
# 设置严格模式
set -e # 命令失败时退出
set -u # 使用未定义变量时退出
set -o pipefail # 管道中任一命令失败时退出
# 脚本配置
SCRIPT_NAME=$(basename "$0")
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
# 帮助信息
usage() {
echo "用法: $SCRIPT_NAME [选项]"
echo "选项:"
echo " -h, --help 显示帮助信息"
echo " -v, --version 显示版本信息"
}
# 主程序
main() {
echo "脚本开始执行"
# 在这里编写主要逻辑
echo "脚本执行完成"
}
# 调用主函数
main "$@"Shebang
bash
#!/bin/bash # 使用 bash 解释器
#!/bin/sh # 使用 sh 解释器(POSIX 兼容)
#!/usr/bin/env bash # 使用 env 查找 bash(更可移植)
#!/usr/bin/python3 # Python 脚本
#!/usr/bin/env node # Node.js 脚本
# 查看可用的 Shell
cat /etc/shells
# 查看默认 Shell
echo $SHELL变量
变量定义和使用
bash
#!/bin/bash
# 变量定义(等号两边不能有空格)
name="张三"
age=25
score=95.5
# 使用变量
echo "姓名:$name"
echo "年龄:$age"
echo "成绩:$score"
# 使用 ${} 明确变量边界
echo "姓名是${name},年龄是${age}岁"
# 变量赋值
current_date=$(date +%Y-%m-%d)
current_time=`date +%H:%M:%S` # 旧式写法
# 只读变量
readonly PI=3.14159
# PI=3.14 # 错误:只读变量不能修改
# 删除变量
unset score
echo "成绩:$score" # 输出空
# 变量默认值
echo "未定义变量:${undefined_var:-默认值}"
echo "未定义变量:${undefined_var:=默认值并赋值}"
# 变量存在时返回值
defined_var="已定义"
echo "变量存在:${defined_var:+存在时返回此值}"数据类型
bash
#!/bin/bash
# 字符串
str1='单引号字符串,变量$HOME不会被替换'
str2="双引号字符串,变量$HOME会被替换"
str3="字符串可以
换行"
# 字符串操作
str="Hello World"
echo "字符串长度:${#str}"
echo "子字符串:${str:0:5}" # 从位置0开始,取5个字符
echo "子字符串:${str:6}" # 从位置6开始到结尾
echo "删除匹配:${str#Hello }" # 删除开头匹配
echo "删除匹配:${str%World}" # 删除结尾匹配
echo "替换:${str/World/Linux}" # 替换第一个匹配
echo "替换全部:${str//o/O}" # 替换所有匹配
# 数组
arr=(one two three four five)
echo "第一个元素:${arr[0]}"
echo "所有元素:${arr[@]}"
echo "所有元素:${arr[*]}"
echo "数组长度:${#arr[@]}"
echo "元素长度:${#arr[0]}"
# 遍历数组
for item in "${arr[@]}"; do
echo "元素:$item"
done
# 关联数组(需要 bash 4.0+)
declare -A user
user[name]="张三"
user[age]=25
user[city]="北京"
echo "姓名:${user[name]}"
echo "所有键:${!user[@]}"
echo "所有值:${user[@]}"
# 遍历关联数组
for key in "${!user[@]}"; do
echo "$key: ${user[$key]}"
done特殊变量
bash
#!/bin/bash
# 特殊变量
echo "脚本名称:$0"
echo "第一个参数:$1"
echo "第二个参数:$2"
echo "参数个数:$#"
echo "所有参数:$@"
echo "所有参数:$*"
echo "上一命令退出状态:$?"
echo "当前进程 PID:$$"
echo "后台进程 PID:$!"
# $@ 和 $* 的区别
echo "--- 使用 \$@ ---"
for arg in "$@"; do
echo "参数:$arg"
done
echo "--- 使用 \$* ---"
for arg in "$*"; do
echo "参数:$arg"
done
# 参数处理
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
echo "显示帮助信息"
shift
;;
-v|--version)
echo "版本 1.0.0"
shift
;;
-*)
echo "未知选项:$1"
exit 1
;;
*)
echo "参数:$1"
shift
;;
esac
done环境变量
bash
#!/bin/bash
# 查看环境变量
echo "HOME: $HOME"
echo "USER: $USER"
echo "PATH: $PATH"
echo "PWD: $PWD"
echo "SHELL: $SHELL"
# 设置环境变量
export MY_VAR="自定义环境变量"
# 在脚本中设置的环境变量只在当前进程及其子进程有效
# 常用环境变量
echo "主机名:$HOSTNAME"
echo "历史命令数:$HISTSIZE"
echo "语言设置:$LANG"
echo "终端类型:$TERM"
# 读取用户输入
read -p "请输入姓名:" name
read -s -p "请输入密码:" password
echo
echo "姓名:$name"
echo "密码长度:${#password}"
# 带超时的输入
read -t 5 -p "5秒内输入:" input || echo "超时"流程控制
条件判断
bash
#!/bin/bash
# if 语句
age=18
if [[ $age -ge 18 ]]; then
echo "成年人"
elif [[ $age -ge 12 ]]; then
echo "青少年"
else
echo "儿童"
fi
# 数值比较
# -eq: 等于
# -ne: 不等于
# -gt: 大于
# -lt: 小于
# -ge: 大于等于
# -le: 小于等于
if [[ $age -eq 18 ]]; then
echo "正好18岁"
fi
# 字符串比较
str1="hello"
str2="world"
if [[ $str1 == $str2 ]]; then
echo "字符串相等"
elif [[ $str1 != $str2 ]]; then
echo "字符串不相等"
fi
# 字符串判空
if [[ -z "$str1" ]]; then
echo "字符串为空"
fi
if [[ -n "$str1" ]]; then
echo "字符串非空"
fi
# 文件测试
file="/etc/passwd"
if [[ -e $file ]]; then echo "文件存在"; fi
if [[ -f $file ]]; then echo "是普通文件"; fi
if [[ -d $file ]]; then echo "是目录"; fi
if [[ -r $file ]]; then echo "文件可读"; fi
if [[ -w $file ]]; then echo "文件可写"; fi
if [[ -x $file ]]; then echo "文件可执行"; fi
if [[ -s $file ]]; then echo "文件非空"; fi
if [[ -L $file ]]; then echo "是符号链接"; fi
# 逻辑运算
if [[ $age -ge 18 && $age -le 60 ]]; then
echo "工作年龄"
fi
if [[ $age -lt 18 || $age -gt 60 ]]; then
echo "非工作年龄"
fi
# 正则匹配
email="user@example.com"
if [[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "有效的邮箱地址"
ficase 语句
bash
#!/bin/bash
# case 语句
read -p "请输入选项 (start/stop/restart/status): " action
case $action in
start)
echo "启动服务"
;;
stop)
echo "停止服务"
;;
restart)
echo "重启服务"
;;
status)
echo "查看状态"
;;
*)
echo "未知选项:$action"
exit 1
;;
esac
# 模式匹配
read -p "请输入文件名:filename
case $filename in
*.txt)
echo "文本文件"
;;
*.sh)
echo "Shell 脚本"
;;
*.py)
echo "Python 脚本"
;;
*.jpg|*.png|*.gif)
echo "图片文件"
;;
*)
echo "未知文件类型"
;;
esac
# 使用 | 分隔多个模式
read -p "请输入字符:" char
case $char in
[a-z]|[A-Z])
echo "字母"
;;
[0-9])
echo "数字"
;;
*)
echo "其他字符"
;;
esac循环语句
bash
#!/bin/bash
# for 循环 - 列表形式
for i in 1 2 3 4 5; do
echo "数字:$i"
done
# for 循环 - 范围形式
for i in {1..5}; do
echo "数字:$i"
done
# for 循环 - 步长
for i in {1..10..2}; do
echo "奇数:$i"
done
# for 循环 - C 风格
for ((i=1; i<=5; i++)); do
echo "计数:$i"
done
# for 循环 - 遍历文件
for file in /etc/*.conf; do
echo "配置文件:$file"
done
# for 循环 - 遍历命令输出
for user in $(cut -d: -f1 /etc/passwd); do
echo "用户:$user"
done
# while 循环
count=1
while [[ $count -le 5 ]]; do
echo "计数:$count"
((count++))
done
# while 循环 - 读取文件
while IFS= read -r line; do
echo "行:$line"
done < /etc/hosts
# while 循环 - 无限循环
while true; do
echo "按 Ctrl+C 退出"
sleep 1
done
# until 循环(条件为假时执行)
count=1
until [[ $count -gt 5 ]]; do
echo "计数:$count"
((count++))
done
# 循环控制
for i in {1..10}; do
if [[ $i -eq 3 ]]; then
continue # 跳过当前迭代
fi
if [[ $i -eq 7 ]]; then
break # 退出循环
fi
echo "数字:$i"
done
# 嵌套循环
for i in {1..3}; do
for j in {1..3}; do
echo "($i, $j)"
done
done函数
函数定义
bash
#!/bin/bash
# 函数定义方式一
function greet() {
echo "Hello, $1!"
}
# 函数定义方式二
say_hello() {
echo "Hello, World!"
}
# 调用函数
greet "张三"
say_hello
# 带返回值的函数
add() {
local result=$(($1 + $2))
echo $result
}
sum=$(add 10 20)
echo "和:$sum"
# 使用 return 返回状态码
check_file() {
if [[ -f $1 ]]; then
return 0 # 成功
else
return 1 # 失败
fi
}
check_file /etc/passwd
if [[ $? -eq 0 ]]; then
echo "文件存在"
fi
# 局部变量
func() {
local local_var="局部变量"
global_var="全局变量"
echo "函数内:$local_var"
}
func
echo "函数外:$global_var"
# echo "函数外:$local_var" # 错误:局部变量不可访问函数参数
bash
#!/bin/bash
# 函数参数
show_args() {
echo "函数名:$0"
echo "参数个数:$#"
echo "所有参数:$@"
echo "第一个参数:$1"
echo "第二个参数:$2"
# 遍历参数
for arg in "$@"; do
echo "参数:$arg"
done
}
show_args "a" "b" "c"
# 参数默认值
greet() {
local name=${1:-"访客"}
echo "你好,$name!"
}
greet # 输出:你好,访客!
greet "张三" # 输出:你好,张三!
# 可变参数
sum() {
local total=0
for num in "$@"; do
((total += num))
done
echo $total
}
result=$(sum 1 2 3 4 5)
echo "总和:$result"递归函数
bash
#!/bin/bash
# 阶乘
factorial() {
local n=$1
if [[ $n -le 1 ]]; then
echo 1
else
local prev=$(factorial $((n - 1)))
echo $((n * prev))
fi
}
result=$(factorial 5)
echo "5! = $result"
# 斐波那契数列
fibonacci() {
local n=$1
if [[ $n -le 1 ]]; then
echo $n
else
local a=$(fibonacci $((n - 1)))
local b=$(fibonacci $((n - 2)))
echo $((a + b))
fi
}
for i in {0..10}; do
result=$(fibonacci $i)
echo "F($i) = $result"
done输入输出
echo 和 printf
bash
#!/bin/bash
# echo 输出
echo "普通输出"
echo -n "不换行输出"
echo "继续输出"
echo -e "转义字符:\t制表符\n换行"
# printf 格式化输出
printf "姓名:%s,年龄:%d\n" "张三" 25
printf "浮点数:%.2f\n" 3.14159
printf "宽度:|%10s|\n" "hello"
printf "左对齐:|%-10s|\n" "hello"
# printf 格式说明符
# %s - 字符串
# %d - 整数
# %f - 浮点数
# %x - 十六进制
# %o - 八进制
# %c - 单个字符
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # 重置颜色
echo -e "${RED}红色文字${NC}"
echo -e "${GREEN}绿色文字${NC}"
echo -e "${YELLOW}黄色文字${NC}"
printf "${RED}错误:${NC}操作失败\n"
printf "${GREEN}成功:${NC}操作完成\n"read 输入
bash
#!/bin/bash
# 基本输入
read -p "请输入姓名:" name
echo "你好,$name!"
# 静默输入(密码)
read -s -p "请输入密码:" password
echo
echo "密码长度:${#password}"
# 限制输入长度
read -n 1 -p "按任意键继续..." key
echo
echo "你按下了:$key"
# 带超时
read -t 10 -p "10秒内输入:" input || echo "超时"
# 读取到数组
read -a arr -p "输入多个单词(空格分隔):"
echo "第一个:${arr[0]}"
echo "所有:${arr[@]}"
# 读取整行
while IFS= read -r line; do
echo "行:$line"
done <<< "第一行
第二行
第三行"
# 从文件读取
while IFS=: read -r user pass uid gid rest; do
echo "用户:$user,UID:$uid"
done < /etc/passwd文件描述符
bash
#!/bin/bash
# 标准文件描述符
# 0 - stdin(标准输入)
# 1 - stdout(标准输出)
# 2 - stderr(标准错误)
# 重定向
echo "标准输出" > output.txt
echo "追加内容" >> output.txt
# 重定向错误
ls /notexist 2> error.txt
# 同时重定向输出和错误
ls / /notexist > all.txt 2>&1
ls / /notexist &> all.txt # 简写
# 丢弃输出
ls /notexist 2>/dev/null
# 自定义文件描述符
exec 3> custom.txt
echo "写入文件描述符3" >&3
exec 3>&- # 关闭
# 从文件读取
exec 3< /etc/hosts
while read -u 3 line; do
echo "读取:$line"
done
exec 3<&- # 关闭
# 同时读写
exec 3<> file.txt
echo "写入" >&3
exec 3<&-实战案例
系统备份脚本
bash
#!/bin/bash
# =====================================
# 脚本名称:backup.sh
# 功能描述:系统备份脚本
# =====================================
set -e
# 配置
BACKUP_DIR="/backup"
SOURCE_DIRS=("/etc" "/home" "/var/www")
DATE=$(date +%Y%m%d_%H%M%S)
LOG_FILE="${BACKUP_DIR}/backup_${DATE}.log"
# 创建备份目录
mkdir -p "$BACKUP_DIR"
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# 备份函数
backup() {
local source=$1
local dest="${BACKUP_DIR}/$(basename $source)_${DATE}.tar.gz"
log "开始备份:$source"
if tar -czf "$dest" "$source" 2>> "$LOG_FILE"; then
log "备份成功:$dest"
log "文件大小:$(du -h "$dest" | cut -f1)"
else
log "备份失败:$source"
return 1
fi
}
# 清理旧备份
cleanup() {
log "清理30天前的备份..."
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +30 -delete
log "清理完成"
}
# 主程序
main() {
log "========== 备份开始 =========="
for dir in "${SOURCE_DIRS[@]}"; do
if [[ -d "$dir" ]]; then
backup "$dir"
else
log "目录不存在:$dir"
fi
done
cleanup
log "========== 备份完成 =========="
}
main "$@"服务监控脚本
bash
#!/bin/bash
# =====================================
# 脚本名称:monitor.sh
# 功能描述:服务监控脚本
# =====================================
# 配置
SERVICES=("nginx" "mysql" "redis")
ALERT_EMAIL="admin@example.com"
LOG_FILE="/var/log/monitor.log"
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# 检查服务状态
check_service() {
local service=$1
if systemctl is-active --quiet "$service"; then
log "$service 运行正常"
return 0
else
log "$服务已停止,尝试重启..."
systemctl restart "$service"
sleep 3
if systemctl is-active --quiet "$service"; then
log "$service 重启成功"
return 0
else
log "$service 重启失败"
send_alert "$service"
return 1
fi
fi
}
# 发送告警
send_alert() {
local service=$1
local message="$service 服务异常,请检查!"
# 发送邮件(需要配置邮件服务)
echo "$message" | mail -s "服务告警:$service" "$ALERT_EMAIL"
log "已发送告警邮件"
}
# 检查磁盘空间
check_disk() {
local threshold=90
local usage=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
if [[ $usage -gt $threshold ]]; then
log "磁盘使用率超过 $threshold%:当前 $usage%"
# 发送告警
fi
}
# 检查内存
check_memory() {
local threshold=90
local usage=$(free | awk '/Mem/ {printf "%.0f", $3/$2 * 100}')
if [[ $usage -gt $threshold ]]; then
log "内存使用率超过 $threshold%:当前 $usage%"
# 发送告警
fi
}
# 主程序
main() {
log "========== 监控开始 =========="
# 检查服务
for service in "${SERVICES[@]}"; do
check_service "$service"
done
# 检查系统资源
check_disk
check_memory
log "========== 监控完成 =========="
}
main "$@"日志分析脚本
bash
#!/bin/bash
# =====================================
# 脚本名称:log_analyze.sh
# 功能描述:日志分析脚本
# =====================================
# 配置
LOG_FILE="/var/log/nginx/access.log"
REPORT_FILE="/tmp/log_report.txt"
# 分析 IP 访问量
analyze_ip() {
echo "=== IP 访问量 TOP 10 ==="
awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
}
# 分析请求路径
analyze_path() {
echo "=== 请求路径 TOP 10 ==="
awk '{print $7}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
}
# 分析状态码
analyze_status() {
echo "=== 状态码统计 ==="
awk '{print $9}' "$LOG_FILE" | sort | uniq -c | sort -rn
}
# 分析访问时间
analyze_time() {
echo "=== 每小时访问量 ==="
awk '{print substr($4, 14, 2)}' "$LOG_FILE" | sort | uniq -c
}
# 分析 User-Agent
analyze_ua() {
echo "=== 浏览器统计 ==="
awk -F'"' '{print $6}' "$LOG_FILE" | grep -oE '(Chrome|Firefox|Safari|Edge|MSIE)' | sort | uniq -c | sort -rn
}
# 生成报告
generate_report() {
{
echo "日志分析报告"
echo "生成时间:$(date)"
echo "日志文件:$LOG_FILE"
echo ""
analyze_ip
echo ""
analyze_path
echo ""
analyze_status
echo ""
analyze_time
echo ""
analyze_ua
} > "$REPORT_FILE"
echo "报告已生成:$REPORT_FILE"
}
# 主程序
main() {
if [[ ! -f "$LOG_FILE" ]]; then
echo "日志文件不存在:$LOG_FILE"
exit 1
fi
generate_report
}
main "$@"小结
本章介绍了 Shell 脚本编程:
| 内容 | 说明 |
|---|---|
| 脚本基础 | Shebang、脚本结构、执行方式 |
| 变量 | 定义、使用、数据类型、特殊变量 |
| 流程控制 | if、case、for、while、until |
| 函数 | 定义、参数、返回值、递归 |
| 输入输出 | echo、printf、read、重定向 |
最佳实践
bash
# 1. 使用严格模式
set -euo pipefail
# 2. 变量使用双引号
echo "$variable"
# 3. 使用 [[ ]] 进行条件判断
if [[ $var == "value" ]]; then
# 4. 函数使用 local 变量
func() {
local var="local"
}
# 5. 添加错误处理
command || exit 1
# 6. 使用有意义的变量名
log_file="/var/log/app.log"
# 7. 添加注释和帮助信息下一步
下一章我们将学习 服务管理,了解 systemd 和服务配置。
