作为一名Linux系统管理员,我每天都要与Shell脚本打交道。Shell脚本是Linux系统管理的利器,掌握好Shell脚本编写能极大提升工作效率。下面我将从基础到高级,系统性地分享Shell脚本编写的核心要点。
每个Shell脚本都应该以shebang开头,这是脚本的第一行,告诉系统使用哪种解释器来执行这个脚本。常见的写法有两种:
bash复制#!/bin/bash # 使用Bash解释器
#!/bin/sh # 使用系统默认Shell解释器
选择哪种解释器取决于脚本的需求。Bash功能更强大,兼容性更好,是我推荐的首选。
注释是脚本中不可或缺的部分,良好的注释能让脚本更易维护:
bash复制# 这是单行注释
: '
这是多行注释的第一行
这是多行注释的第二行
'
<<EOF
这也是多行注释的一种写法
可以自由换行
EOF
变量是存储数据的容器,Shell中的变量定义和使用有以下几个要点:
bash复制name="张三" # 定义变量,等号两边不能有空格
readonly PI=3.14159 # 定义只读变量
unset name # 删除变量(对只读变量无效)
current_dir=$(pwd) # 命令替换,获取命令执行结果
num=$((18+5)) # 算术运算,使用双括号
变量引用时,建议使用${}明确变量边界:
bash复制file="document"
echo "${file}_backup" # 输出document_backup
变量操作的高级用法:
bash复制# 默认值处理
echo ${name:-"默认值"} # 变量为空时使用默认值,不改变原变量
echo ${name:="默认值"} # 变量为空时赋值并使用默认值
# 字符串操作
str="hello world"
echo ${#str} # 获取字符串长度
echo ${str:6:5} # 从第6个字符开始截取5个字符
echo ${str/hello/hi} # 替换第一个匹配的子串
echo ${str//l/L} # 替换所有匹配的子串
环境变量是系统预定义的全局变量,常用的有:
bash复制echo $PATH # 命令搜索路径
echo $HOME # 当前用户主目录
echo $PWD # 当前工作目录
echo $USER # 当前用户名
echo $SHELL # 当前使用的Shell
echo $? # 上一条命令的退出状态(0表示成功)
修改PATH环境变量的正确方式:
bash复制# 临时修改(当前会话有效)
export PATH=$PATH:/new/path
# 永久修改(对当前用户)
echo 'export PATH=$PATH:/new/path' >> ~/.bashrc
source ~/.bashrc
# 永久修改(对所有用户)
echo 'export PATH=$PATH:/new/path' >> /etc/profile
source /etc/profile
echo命令是脚本中最常用的输出命令:
bash复制echo "Hello World" # 默认带换行
echo -n "No new line" # -n取消末尾换行
echo -e "Line1\nLine2" # -e启用转义字符
read命令用于获取用户输入:
bash复制read -p "请输入用户名: " username
read -s -p "请输入密码: " password # -s隐藏输入
read -t 5 -p "5秒内输入: " input # -t设置超时
重定向改变了命令输入输出的默认流向:
bash复制echo "test" > file.txt # 覆盖输出到文件
echo "append" >> file.txt # 追加输出到文件
command < input.txt # 从文件读取输入
command 2> error.log # 将错误输出重定向到文件
command > output.log 2>&1 # 标准输出和错误输出都重定向
管道将一个命令的输出作为另一个命令的输入:
bash复制ps -ef | grep java # 查找Java进程
cat access.log | awk '{print $1}' | sort | uniq -c # 统计IP访问次数
Shell支持索引数组和关联数组:
bash复制# 索引数组
fruits=("apple" "banana" "orange")
echo ${fruits[1]} # 输出banana
echo ${fruits[@]} # 输出所有元素
echo ${#fruits[@]} # 输出数组长度
# 关联数组
declare -A user
user=([name]="张三" [age]=30)
echo ${user[name]} # 输出张三
if语句是脚本中最常用的条件判断结构:
bash复制# 基本格式
if [ condition ]; then
commands
elif [ condition ]; then
commands
else
commands
fi
# 文件判断示例
file="/path/to/file"
if [ -f "$file" ]; then
echo "文件存在"
elif [ -d "$file" ]; then
echo "这是个目录"
else
echo "文件不存在"
fi
# 数值比较
num=10
if [ $num -gt 5 ]; then
echo "大于5"
fi
# 字符串匹配
str="hello world"
if [[ $str == *hello* ]]; then
echo "包含hello"
fi
正则表达式是强大的文本匹配工具:
bash复制# 手机号校验
phone="13812345678"
if [[ $phone =~ ^1[3-9][0-9]{9}$ ]]; then
echo "有效手机号"
fi
# 邮箱校验
email="test@example.com"
if [[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$ ]]; then
echo "有效邮箱"
fi
# 字符串提取
text="订单号:20260119,金额:999元"
if [[ $text =~ [0-9]+ ]]; then
echo "找到数字: ${BASH_REMATCH[0]}"
fi
# 字符串脱敏
phone="13812345678"
masked=${phone:0:3}****${phone:7:4}
echo $masked # 输出138****5678
case语句适合多分支选择场景:
bash复制read -p "输入操作(start/stop/restart): " action
case $action in
start)
echo "启动服务"
;;
stop)
echo "停止服务"
;;
restart)
echo "重启服务"
;;
*)
echo "无效操作"
;;
esac
Shell提供了多种循环结构:
bash复制# for循环
for i in {1..5}; do
echo "数字: $i"
done
# while循环
count=1
while [ $count -le 5 ]; do
echo "计数: $count"
((count++))
done
# until循环(与while条件相反)
count=1
until [ $count -gt 5 ]; do
echo "计数: $count"
((count++))
done
# 文件逐行读取
while IFS= read -r line; do
echo "行内容: $line"
done < file.txt
# 循环控制
for i in {1..10}; do
if [ $i -eq 5 ]; then
break # 跳出循环
fi
if [ $i -eq 3 ]; then
continue # 跳过本次循环
fi
echo $i
done
函数是代码复用的基本单元:
bash复制# 函数定义
function greet() {
local name=$1 # local定义局部变量
echo "Hello, $name"
return 0 # 返回值(0-255)
}
# 函数调用
greet "张三"
result=$? # 获取函数返回值
# 带返回值的函数
add() {
local sum=$(($1 + $2))
echo $sum # 通过echo返回结果
}
total=$(add 3 5) # 获取函数输出
脚本和函数都可以接收参数:
bash复制# 脚本参数
echo "脚本名: $0"
echo "第一个参数: $1"
echo "参数个数: $#"
echo "所有参数: $@"
# 参数移位
shift # 左移参数列表
echo "新的第一个参数: $1"
# 遍历所有参数
for arg in "$@"; do
echo "参数: $arg"
done
Shell提供了许多强大的高级特性:
bash复制# 命令替换
files=$(ls) # 反引号` `也可以,但不推荐
count=$(wc -l < file.txt)
# 算术扩展
result=$((3 + 5 * 2))
((result++)) # 自增运算
# 调试模式
set -x # 开启调试
echo "调试信息"
set +x # 关闭调试
# 错误处理
set -euo pipefail # 严格模式
command || { # 命令失败处理
echo "命令执行失败"
exit 1
}
# 后台任务
sleep 10 & # 后台运行
jobs # 查看后台任务
fg %1 # 将任务调到前台
编写高效的Shell脚本需要注意以下几点:
bash复制# 1. 减少子进程创建
# 不好的写法: 每次调用都会创建子进程
for i in $(seq 1 100); do
echo $i
done
# 好的写法: 使用内置语法
for i in {1..100}; do
echo $i
done
# 2. 避免不必要的cat
# 不好的写法
cat file.txt | grep "pattern"
# 好的写法
grep "pattern" file.txt
# 3. 使用here document替代echo
# 不好的写法
echo "line1" > file
echo "line2" >> file
# 好的写法
cat > file << EOF
line1
line2
EOF
# 4. 使用awk/sed处理文本
# 低效写法
while read line; do
# 处理每行
done < file.txt
# 高效写法
awk '{print $1}' file.txt
编写安全的Shell脚本需要注意:
bash复制#!/bin/bash
set -euo pipefail # 启用严格模式
# 1. 变量引用加双引号
filename="my file.txt"
rm "$filename" # 正确处理含空格的文件名
# 2. 检查文件是否存在
if [ ! -f "$file" ]; then
echo "错误: 文件不存在" >&2
exit 1
fi
# 3. 检查命令是否存在
command -v git >/dev/null || {
echo "错误: git未安装" >&2
exit 1
}
# 4. 检查目录权限
if [ ! -w "/tmp" ]; then
echo "错误: 无写权限" >&2
exit 1
fi
# 5. 敏感信息处理
password="secret"
unset password # 使用后立即清除
完善的错误处理能让脚本更健壮:
bash复制# 1. 捕获信号
trap 'cleanup; exit 1' INT TERM EXIT
# 2. 自定义错误处理
error() {
echo "错误: $1" >&2
exit 1
}
# 3. 检查命令返回值
mkdir /path || error "创建目录失败"
# 4. 日志记录
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> script.log
}
log "脚本开始执行"
# 5. 清理函数
cleanup() {
rm -f tempfile
log "脚本执行结束"
}
良好的代码组织提升可维护性:
bash复制#!/bin/bash
# 脚本名称: system_backup.sh
# 描述: 系统备份脚本
# 作者: Your Name
# 版本: 1.0
# 修改记录:
# 2023-01-01 - 初始版本
# === 配置部分 ===
BACKUP_DIR="/backup"
LOG_FILE="/var/log/backup.log"
MAX_DAYS=30
# === 函数定义 ===
init_check() {
[ "$(id -u)" -eq 0 ] || {
echo "请使用root用户运行" >&2
return 1
}
[ -d "$BACKUP_DIR" ] || mkdir -p "$BACKUP_DIR"
}
backup_files() {
local src=$1
local dest="${BACKUP_DIR}/$(basename $src).tgz"
tar -czf "$dest" "$src" && echo "备份成功: $dest"
}
clean_old() {
find "$BACKUP_DIR" -type f -mtime +$MAX_DAYS -delete
}
# === 主程序 ===
main() {
init_check || exit 1
backup_files "/etc"
backup_files "/home"
clean_old
}
# 执行入口
main "$@"
bash复制#!/bin/bash
# 日志分析脚本
LOG_FILE="/var/log/nginx/access.log"
REPORT_FILE="/tmp/access_report_$(date +%Y%m%d).txt"
# 分析函数
analyze() {
echo "===== NGINX访问日志分析报告 =====" > "$REPORT_FILE"
echo "生成时间: $(date '+%Y-%m-%d %H:%M:%S')" >> "$REPORT_FILE"
echo "=================================" >> "$REPORT_FILE"
# 总访问量
total=$(wc -l < "$LOG_FILE")
echo "1. 总访问量: $total" >> "$REPORT_FILE"
# 独立IP统计
echo "" >> "$REPORT_FILE"
echo "2. 独立IP访问TOP10:" >> "$REPORT_FILE"
awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -nr | head -10 >> "$REPORT_FILE"
# 状态码统计
echo "" >> "$REPORT_FILE"
echo "3. HTTP状态码统计:" >> "$REPORT_FILE"
awk '{print $9}' "$LOG_FILE" | sort | uniq -c | sort -nr >> "$REPORT_FILE"
# 热门URL
echo "" >> "$REPORT_FILE"
echo "4. 热门URL TOP10:" >> "$REPORT_FILE"
awk '{print $7}' "$LOG_FILE" | sort | uniq -c | sort -nr | head -10 >> "$REPORT_FILE"
# 流量统计
echo "" >> "$REPORT_FILE"
echo "5. 总流量统计:" >> "$REPORT_FILE"
awk '{sum+=$10} END {print sum/1024/1024 " MB"}' "$LOG_FILE" >> "$REPORT_FILE"
}
# 主程序
main() {
[ -f "$LOG_FILE" ] || {
echo "错误: 日志文件不存在" >&2
exit 1
}
analyze
echo "分析完成,报告已生成: $REPORT_FILE"
}
main "$@"
bash复制#!/bin/bash
# 系统监控脚本
INTERVAL=5 # 监控间隔(秒)
LOG_FILE="/var/log/system_monitor.log"
# 监控函数
monitor() {
while true; do
# 获取系统指标
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local cpu=$(top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4}')
local mem_free=$(free -m | awk '/Mem/{print $4}')
local disk_usage=$(df -h / | awk 'NR==2{print $5}')
local load_avg=$(cat /proc/loadavg | awk '{print $1,$2,$3}')
# 写入日志
echo "$timestamp CPU: ${cpu}% 可用内存: ${mem_free}MB 根分区使用: $disk_usage 负载: $load_avg" >> "$LOG_FILE"
# 检查阈值
if (( $(echo "$cpu > 90" | bc -l) )); then
echo "警告: CPU使用率过高 - ${cpu}%" >&2
fi
if [ "$mem_free" -lt 100 ]; then
echo "警告: 可用内存不足 - ${mem_free}MB" >&2
fi
sleep "$INTERVAL"
done
}
# 主程序
main() {
echo "===== 系统监控开始 =====" >> "$LOG_FILE"
monitor
}
main "$@"
bash复制#!/bin/bash
# 应用自动化部署脚本
APP_NAME="myapp"
APP_DIR="/opt/$APP_NAME"
BACKUP_DIR="/backup/$APP_NAME"
GIT_REPO="https://github.com/example/myapp.git"
CONFIG_FILE="/etc/$APP_NAME.conf"
# 初始化检查
init_check() {
[ -d "$APP_DIR" ] || mkdir -p "$APP_DIR"
[ -d "$BACKUP_DIR" ] || mkdir -p "$BACKUP_DIR"
command -v git >/dev/null || {
echo "错误: git未安装" >&2
return 1
}
[ -f "$CONFIG_FILE" ] || {
echo "错误: 配置文件不存在" >&2
return 1
}
}
# 备份当前版本
backup() {
local backup_file="${BACKUP_DIR}/${APP_NAME}_$(date +%Y%m%d%H%M%S).tgz"
tar -czf "$backup_file" -C "$APP_DIR" . && echo "备份成功: $backup_file"
}
# 从Git拉取代码
update_code() {
if [ -d "$APP_DIR/.git" ]; then
git -C "$APP_DIR" pull origin master
else
git clone "$GIT_REPO" "$APP_DIR"
fi
}
# 安装依赖
install_deps() {
if [ -f "$APP_DIR/requirements.txt" ]; then
pip install -r "$APP_DIR/requirements.txt"
fi
}
# 重启服务
restart_service() {
systemctl restart "$APP_NAME" || {
echo "警告: 服务重启失败" >&2
return 1
}
}
# 主部署流程
deploy() {
init_check || exit 1
backup
update_code
install_deps
cp "$CONFIG_FILE" "$APP_DIR/config/"
restart_service
}
# 执行部署
deploy
调试是脚本开发的重要环节:
bash复制#!/bin/bash -x # 直接在shebang开启调试
# 局部调试
set -x
# 要调试的代码块
set +x
# 输出调试信息
echo "调试: 变量值=$var" >&2 # 输出到标准错误
# 使用trap调试
trap 'echo "在行: $LINENO 变量: $var"' DEBUG
# 检查命令执行
command || echo "命令失败: $?" >&2
# 使用bashdb调试器
# 安装: apt-get install bashdb
# 使用: bashdb script.sh
优化脚本性能的几个方向:
bash复制# 1. 测量脚本执行时间
time ./script.sh
# 2. 分析瓶颈命令
strace -c ./script.sh # 系统调用分析
ltrace -c ./script.sh # 库调用分析
# 3. 减少子进程创建
# 不好的写法: 每次循环都创建子进程
for i in $(seq 1 1000); do
echo $i
done
# 好的写法: 使用内置功能
for i in {1..1000}; do
echo $i
done
# 4. 使用更高效的命令组合
# 不好的写法: 多次调用grep
grep "error" log.txt | grep -v "warning"
# 好的写法: 使用awk单次处理
awk '/error/ && !/warning/' log.txt
# 5. 并行处理加速
for file in *.log; do
process "$file" &
done
wait # 等待所有后台任务完成
Shell脚本中常见问题及解决方法:
bash复制set -u # 开启未定义变量检测
echo "$undefined_var" # 会报错退出
bash复制# 正确的比较写法
if [ "$var" = "value" ]; then
# 注意[]内空格
fi
bash复制# 错误写法
for file in $(ls); do
# 如果文件名有空格会出错
done
# 正确写法
while IFS= read -r -d '' file; do
# 处理每个文件
done < <(find . -type f -print0)
bash复制set -o pipefail # 管道中任一命令失败则整个管道失败
false | true # 正常情况下会返回0,开启pipefail后返回1
bash复制# Bash本身不支持浮点运算,需要借助外部工具
result=$(echo "scale=2; 3/7" | bc)
编写安全的Shell脚本需要注意:
bash复制#!/bin/bash
set -euo pipefail # 启用严格模式
# 1. 检查输入参数
[ "$#" -ge 2 ] || {
echo "用法: $0 <参数1> <参数2>" >&2
exit 1
}
# 2. 检查文件权限
[ -w "/tmp" ] || {
echo "错误: 无写权限" >&2
exit 1
}
# 3. 清理敏感信息
password="secret"
# 使用密码...
unset password # 使用后立即清除
# 4. 避免命令注入
user_input="; rm -rf /"
# 错误写法: 直接执行用户输入
# echo $user_input | sh
# 正确写法: 严格过滤输入
if [[ "$user_input" =~ ^[a-zA-Z0-9]+$ ]]; then
echo "$user_input"
else
echo "非法输入" >&2
fi
# 5. 使用临时文件安全方式
tempfile=$(mktemp /tmp/script.XXXXXX)
trap 'rm -f "$tempfile"' EXIT # 脚本退出时删除
保持脚本可维护性的建议:
bash复制# 函数: 备份目录
# 参数: $1 - 要备份的目录路径
# 返回值: 0-成功, 1-失败
backup_dir() {
# 函数实现...
}
bash复制#!/bin/bash
# 脚本名称: deploy.sh
# 版本: 1.2
# 修改记录:
# 2023-01-01 v1.0 - 初始版本
# 2023-02-15 v1.1 - 添加错误处理
# 2023-03-20 v1.2 - 优化性能
bash复制# 配置文件
source config.sh
# 工具函数
source utils.sh
# 主程序
main() {
init_check
process_data
generate_report
}
bash复制log() {
local level=$1
local message=$2
echo "$(date '+%Y-%m-%d %H:%M:%S') [$level] $message" >> "$LOG_FILE"
}
log "INFO" "脚本启动"
log "ERROR" "发生错误"
bash复制# 测试函数
test_add() {
result=$(add 2 3)
[ "$result" -eq 5 ] || {
echo "测试失败: 2 + 3 应该等于5, 实际得到 $result" >&2
return 1
}
echo "测试通过"
}
# 运行测试
test_add
编写可移植的Shell脚本:
bash复制#!/bin/sh # 使用POSIX标准的Shell
# 1. 避免Bash特有语法
# 使用test代替[[ ]]
if test "$var" = "value"; then
# ...
fi
# 2. 处理不同系统的命令差异
case "$(uname -s)" in
Linux*) machine=Linux;;
Darwin*) machine=Mac;;
CYGWIN*) machine=Cygwin;;
MINGW*) machine=MinGw;;
*) machine="UNKNOWN"
esac
# 3. 处理不同系统的路径差异
PATH="/usr/local/bin:/usr/bin:/bin"
[ "$machine" = "Mac" ] && PATH="/opt/homebrew/bin:$PATH"
# 4. 检查命令是否存在
check_command() {
command -v "$1" >/dev/null 2>&1 || {
echo "错误: 需要安装 $1" >&2
exit 1
}
}
check_command awk
check_command sed
bash复制declare -A user
user=([name]="张三" [age]=30 [email]="zhangsan@example.com")
echo "用户名: ${user[name]}"
bash复制# 比较两个文件的差异
diff <(sort file1) <(sort file2)
bash复制coproc myproc {
while read -r line; do
echo "处理: $line"
done
}
echo "输入数据" >&${myproc[1]}
read -r output <&${myproc[0]}
echo "输出: $output"
bash复制mkfifo mypipe
echo "数据" > mypipe &
cat mypipe
bash复制trap 'cleanup; exit' INT TERM EXIT
cleanup() {
echo "清理临时文件..."
rm -f tempfile
}
info bash 或 man bash经过多年的Shell脚本编写实践,我总结了以下几点经验:
保持简单直接
Shell脚本最适合完成中小型自动化任务,对于复杂逻辑建议使用Python等高级语言。
重视错误处理
完善的错误处理能让脚本更健壮,特别是在生产环境中。
编写可读的代码
良好的注释、合理的函数划分和一致的代码风格能大大提升脚本的可维护性。
安全第一
特别是处理用户输入或敏感数据时,要特别注意安全性。
持续学习
Shell的功能非常丰富,即使是经验丰富的开发者也能不断学到新技巧。
最后分享一个实用的小技巧 - 快速生成随机密码:
bash复制generate_password() {
local length=${1:-16}
tr -dc 'A-Za-z0-9!@#$%^&*()' < /dev/urandom | head -c $length
}
希望这篇全面的Shell脚本指南能帮助你提升Linux系统管理效率。记住,实践是最好的学习方式,多写多调试才能真正掌握Shell脚本编程的精髓。