Shell脚本是系统管理和自动化任务的利器。本文将带你从基础到高级,全面掌握Shell脚本编程的最佳实践。
Shell基础
第一个Shell脚本
#!/bin/bash
# 这是一个注释
echo "Hello, World!"
# 变量赋值和使用
name="World"
echo "Hello, $name!"
# 命令替换
current_date=$(date)
echo "Today is: $current_date"
# 反引号方式(不推荐)
current_date=`date`
echo "Today is: $current_date"
Shebang说明:
#!/bin/bash:使用bash解释器#!/bin/sh:使用sh解释器(更通用)#!/usr/bin/env bash:自动查找bash(更便携)
变量和数据类型
# 字符串变量
greeting="Hello"
name="Alice"
# 只读变量
readonly PI=3.14159
# 删除变量
unset name
# 环境变量
export PATH=$PATH:/new/path
# 字符串拼接
fullname="John $greeting"
echo $fullname
# 获取字符串长度
string="Hello, World"
echo ${#string} # 13
# 字符串切片
echo ${string:0:5} # Hello
echo ${string:7} # World
# 默认值
echo ${name:-"Guest"} # 如果name未设置或为空,使用"Guest"
# 数组
arr=(apple banana cherry)
echo ${arr[0]} # apple
echo ${arr[@]} # 所有元素
echo ${#arr[@]} # 数组长度
arr[3]="date" # 添加元素
unset arr[1] # 删除元素
控制结构
条件判断
# if语句
if [ "$name" == "Alice" ]; then
echo "Welcome, Alice!"
elif [ "$name" == "Bob" ]; then
echo "Welcome, Bob!"
else
echo "Welcome, Guest!"
fi
# 数字比较
count=10
if [ $count -eq 10 ]; then
echo "Count is 10"
fi
if [ $count -gt 5 ]; then
echo "Count is greater than 5"
fi
if [ $count -lt 20 ]; then
echo "Count is less than 20"
fi
# 字符串比较
if [ "$string1" == "$string2" ]; then
echo "Strings are equal"
fi
if [ -n "$string" ]; then
echo "String is not empty"
fi
# 文件测试
if [ -f "file.txt" ]; then
echo "File exists and is a regular file"
fi
if [ -d "/tmp" ]; then
echo "Directory exists"
fi
if [ -r "file.txt" ]; then
echo "File is readable"
fi
if [ -w "file.txt" ]; then
echo "File is writable"
fi
if [ -x "script.sh" ]; then
echo "File is executable"
fi
# 逻辑运算
if [ $count -gt 5 ] && [ $count -lt 20 ]; then
echo "Count is between 5 and 20"
fi
if [ $count -lt 5 ] || [ $count -gt 20 ]; then
echo "Count is outside range 5-20"
fi
# 使用test命令
if test -f "file.txt"; then
echo "File exists"
fi
# 双括号(更强大的算术比较)
if (( count > 5 && count < 20 )); then
echo "Count is between 5 and 20"
fi
循环结构
# for循环
for i in 1 2 3 4 5; do
echo $i
done
# 遍历文件
for file in *.txt; do
echo "Processing: $file"
done
# C风格for循环
for ((i=0; i<10; i++)); do
echo $i
done
# while循环
count=0
while [ $count -lt 5 ]; do
echo $count
count=$((count + 1))
done
# 读取文件行
while IFS= read -r line; do
echo "$line"
done < file.txt
# until循环
count=0
until [ $count -ge 5 ]; do
echo $count
count=$((count + 1))
done
# break和continue
for i in {1..10}; do
if [ $i -eq 5 ]; then
continue # 跳过5
fi
if [ $i -eq 8 ]; then
break # 在8处停止
fi
echo $i
done
case语句
# 简单的case语句
read -p "Enter a color: " color
case $color in
red)
echo "You chose red"
;;
blue|green)
echo "You chose blue or green"
;;
*)
echo "You chose something else"
;;
esac
# 复杂的case语句
case $1 in
start)
echo "Starting service..."
;;
stop)
echo "Stopping service..."
;;
restart)
echo "Restarting service..."
;;
status)
echo "Checking service status..."
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
函数编程
定义和使用函数
# 定义函数
greet() {
echo "Hello, $1!"
}
# 调用函数
greet "Alice"
# 返回值
add() {
local result=$(($1 + $2))
echo $result
}
sum=$(add 5 3)
echo "Sum: $sum"
# 返回状态码
check_file() {
if [ -f "$1" ]; then
return 0 # 成功
else
return 1 # 失败
fi
}
if check_file "file.txt"; then
echo "File exists"
else
echo "File does not exist"
fi
# 局部变量
global_var="I am global"
my_function() {
local local_var="I am local"
echo "Inside function: $local_var"
echo "Inside function: $global_var"
}
my_function
echo "Outside function: $global_var"
# echo "Outside function: $local_var" # 错误:local_var未定义
函数参数
# 处理多个参数
process_args() {
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $@"
echo "Number of arguments: $#"
echo "Script name: $0"
}
process_args arg1 arg2 arg3
# 遍历所有参数
iterate_args() {
for arg in "$@"; do
echo "Processing: $arg"
done
}
iterate_args file1.txt file2.txt file3.txt
# shift命令
shift_test() {
echo "Total arguments: $#"
echo "First: $1"
shift
echo "After shift, first: $1"
echo "Remaining arguments: $#"
}
shift_test a b c d
递归函数
# 阶乘(尾递归)
factorial() {
local n=$1
local acc=${2:-1}
if [ $n -le 1 ]; then
echo $acc
else
factorial $((n - 1)) $((acc * n))
fi
}
echo "Factorial of 5: $(factorial 5)"
# Fibonacci
fibonacci() {
local n=$1
if [ $n -le 1 ]; then
echo $n
else
echo $(( $(fibonacci $((n - 1))) + $(fibonacci $((n - 2))) ))
fi
}
echo "Fibonacci of 10: $(fibonacci 10)"
输入输出
读取用户输入
# 简单输入
read -p "Enter your name: " name
echo "Hello, $name!"
# 密码输入(不显示)
read -s -p "Enter password: " password
echo
# 带超时的输入
read -t 5 -p "Enter your choice (5 seconds): " choice
echo "You chose: $choice"
# 读取多个值
read -p "Enter name age: " name age
echo "Name: $name, Age: $age"
# 从文件读取
while IFS= read -r line; do
echo "Line: $line"
done < input.txt
# 读取确认
read -p "Continue? (y/n): " confirm
if [[ $confirm == [yY] ]]; then
echo "Continuing..."
else
echo "Aborting..."
exit 1
fi
输出格式化
# echo选项
echo -n "No newline" # 不换行
echo -e "Line1\nLine2" # 解释转义字符
echo "Hello\tWorld" # 需要配合-e
# printf格式化输出
printf "Name: %s, Age: %d\n" "Alice" 25
printf "Pi: %.2f\n" 3.14159
printf "%-10s %10s\n" "Left" "Right"
# 重定向输出
echo "Error message" >&2 # 输出到stderr
echo "Log message" >> logfile # 追加到文件
# 管道
echo "Hello World" | tr '[:upper:]' '[:lower:]'
# Here文档
cat << EOF
This is a multi-line
string using Here document.
EOF
# Here字符串
grep "pattern" <<< "This is a string to search"
命令行参数
处理位置参数
#!/bin/bash
# script.sh
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $@"
echo "Number of arguments: $#"
# 检查参数数量
if [ $# -lt 2 ]; then
echo "Usage: $0 <arg1> <arg2>"
exit 1
fi
使用getopts
#!/bin/bash
# 使用getopts处理选项
usage() {
echo "Usage: $0 [-a] [-b VALUE] [-c] filename"
exit 1
}
while getopts ":ab:c" opt; do
case $opt in
a)
echo "Option -a triggered"
;;
b)
echo "Option -b triggered with value: $OPTARG"
value=$OPTARG
;;
c)
echo "Option -c triggered"
;;
\?)
echo "Invalid option: -$OPTARG"
usage
;;
:)
echo "Option -$OPTARG requires an argument"
usage
;;
esac
done
shift $((OPTIND-1))
echo "Remaining arguments: $@"
使用getopt(更强大)
#!/bin/bash
# 使用getopt处理长选项
TEMP=$(getopt -o ab:c:: --long alpha,bravo:,charlie:: -n 'example.sh' -- "$@")
if [ $? != 0 ]; then
echo "Terminating..." >&2
exit 1
fi
eval set -- "$TEMP"
while true; do
case "$1" in
-a|--alpha)
echo "Option a"
shift
;;
-b|--bravo)
echo "Option b, argument '$2'"
shift 2
;;
-c|--charlie)
case "$2" in
"")
echo "Option c, no argument"
shift 2
;;
*)
echo "Option c, argument '$2'"
shift 2
;;
esac
;;
--)
shift
break
;;
*)
echo "Internal error!"
exit 1
;;
esac
done
echo "Remaining arguments:"
for arg in "$@"; do
echo " --> '$arg'"
done
信号处理
捕获中断
#!/bin/bash
# 捕获Ctrl+C
cleanup() {
echo "Cleaning up..."
# 删除临时文件等
rm -f /tmp/my_script_temp*
exit 1
}
trap cleanup SIGINT SIGTERM
echo "Press Ctrl+C to interrupt..."
for i in {1..100}; do
echo "Working... $i"
sleep 1
done
捕获EXIT信号
#!/bin/bash
# 确保清理代码总是执行
cleanup() {
echo "Script is exiting..."
rm -f /tmp/tempfile
}
trap cleanup EXIT
# 创建临时文件
touch /tmp/tempfile
echo "Doing some work..."
# 即使脚本出错,cleanup也会执行
文本处理
文件操作
# 读取文件
while IFS= read -r line; do
echo "$line"
done < file.txt
# 写入文件
echo "Hello" > output.txt
echo "World" >> output.txt
# 检查文件是否存在
if [ -f "file.txt" ]; then
echo "File exists"
fi
# 检查文件是否可读
if [ -r "file.txt" ]; then
echo "File is readable"
fi
# 获取文件大小
size=$(wc -c < file.txt)
echo "File size: $size bytes"
# 获取行数
lines=$(wc -l < file.txt)
echo "File lines: $lines"
文本转换
# 转换为大写
echo "hello" | tr '[:lower:]' '[:upper:]'
# 删除重复行
sort file.txt | uniq
# 只显示重复行
sort file.txt | uniq -d
# 统计重复次数
sort file.txt | uniq -c
# 替换文本
sed 's/old/new/g' file.txt
# 删除空行
sed '/^$/d' file.txt
# 提取特定列
awk '{print $1, $3}' file.txt
# 按模式分割文件
awk '/pattern/{filename="part_"++count".txt"; print > filename}'
进程管理
后台执行
# 后台运行
command &
# 后台运行并重定向输出
command > /dev/null 2>&1 &
# 使用nohup(退出终端后继续运行)
nohup command &
# 查看后台任务
jobs
# 带回后台任务
fg %1
# 继续后台任务
bg %1
# 杀死后台任务
kill %1
进程监控
# 查看进程
ps aux
# 查找特定进程
ps aux | grep nginx
# 实时监控
top
# 杀死进程
kill PID
kill -9 PID # 强制杀死
# 等待进程完成
wait PID
调试技巧
调试模式
#!/bin/bash
# 启用调试模式
set -x # 在执行前打印命令
set -v # 打印输入行
# 或者
bash -x script.sh
# 只调试部分代码
set -x # 开始调试
# 需要调试的代码
set +x # 结束调试
错误处理
#!/bin/bash
# 遇到错误立即退出
set -e
# 使用未定义变量时报错
set -u
# 管道命令失败时退出
set -o pipefail
# 组合使用
set -euo pipefail
# 捕获错误
trap 'echo "Error on line $LINENO"; exit 1' ERR
日志记录
#!/bin/bash
# 日志函数
log() {
local level=$1
shift
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $@" | tee -a script.log
}
log INFO "Script started"
log ERROR "An error occurred"
log WARNING "This is a warning"
实用示例
系统监控脚本
#!/bin/bash
# 系统监控脚本
while true; do
clear
echo "=== System Monitor ==="
echo "Time: $(date)"
echo
# CPU使用率
echo "CPU Usage:"
top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1"%"}'
# 内存使用
echo -e "\nMemory Usage:"
free -h
# 磁盘使用
echo -e "\nDisk Usage:"
df -h
sleep 5
done
日志分析脚本
#!/bin/bash
# Apache日志分析
log_file="/var/log/apache2/access.log"
echo "=== Top 10 IPs ==="
awk '{print $1}' "$log_file" | sort | uniq -c | sort -rn | head
echo -e "\n=== Top 10 URLs ==="
awk '{print $7}' "$log_file" | sort | uniq -c | sort -rn | head
echo -e "\n=== HTTP Status Codes ==="
awk '{print $9}' "$log_file" | sort | uniq -c | sort -rn
自动备份脚本
#!/bin/bash
# 自动备份脚本
SOURCE_DIR="/path/to/source"
BACKUP_DIR="/path/to/backup"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_$DATE.tar.gz"
# 创建备份
echo "Creating backup..."
tar -czf "$BACKUP_DIR/$BACKUP_NAME" "$SOURCE_DIR"
# 删除30天前的备份
find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +30 -delete
echo "Backup completed: $BACKUP_NAME"
最佳实践
代码风格
- 使用Shebang:始终在脚本开头指定解释器
- 添加注释:解释复杂逻辑和重要步骤
- 使用有意义的变量名:避免单字母变量(除循环变量外)
- 缩进代码:使用一致的缩进(通常是4个空格)
- 引用变量:始终使用引号包裹变量("$var"而非$var)
安全建议
- 验证输入:始终验证用户输入和参数
- 使用绝对路径:避免路径混淆
- 最小权限原则:只授予必要的权限
- 清理临时文件:脚本结束时清理
- 避免eval:除非绝对必要,否则不使用eval
性能优化
- 避免外部命令:尽量使用内置功能
- 减少子shell:避免不必要的进程创建
- 使用管道:而不是临时文件
- 批量处理:一次处理多个项目
- 缓存结果:避免重复计算
小结
Shell脚本是系统管理和自动化的强大工具。通过本文,你学习了:
- 基础语法:变量、控制结构、函数
- 输入输出:参数处理、用户交互
- 文件操作:读写、文本处理
- 进程管理:后台任务、信号处理
- 调试技巧:错误处理、日志记录
- 最佳实践:代码风格、安全建议
掌握Shell脚本编程,你将能够:
- 自动化重复任务
- 管理系统和服务
- 处理文本和数据
- 创建自定义工具
记住,Shell脚本的关键在于简单和实用。开始时保持简单,随着经验的积累,逐步掌握更高级的技巧。
