Bash脚本编程速成教程
2025年2月27日大约 9 分钟
Bash脚本编程速成教程
用了好几年的Linux才真正开始系统地学习Bash脚本。之前都是现查现用,东拼西凑。一直想写个教程来巩固下自己的知识一直没能有机会。
先给个在线练习环境:
- Katacoda - 免费Linux练习环境
- repl.it - 在线Bash编辑器
- Learn Shell - 交互式Shell学习平台
第一部分:基础概念
Hello Bash!
第一个脚本总得是Hello World:
#!/bin/bash
echo "Hello, World!"
关于Shebang
第一行的 #!/bin/bash
叫做shebang(也叫hashbang),告诉系统用什么解释器执行这个脚本。
变量和数据类型
Bash里的变量...说来有点意思。不像其他编程语言那样严格:
# 变量赋值(等号两边不能有空格,这点老让我踩坑)
name="Alice"
age=25
# 使用变量要加$
echo "Name: $name, Age: $age"
# 这么写也行,更明确一点
echo "Name: ${name}, Age: ${age}"
变量名规则
- 字母、数字、下划线
- 不能数字开头
- 大小写敏感
特殊变量
这些特殊变量我一开始真是记不住:
$0
- 脚本名称$1
,$2
, ... - 位置参数$#
- 参数个数$@
- 所有参数$?
- 上一条命令的退出状态$$
- 当前进程ID
来个例子体会一下:
#!/bin/bash
echo "脚本名称: $0"
echo "第一个参数: $1"
echo "参数个数: $#"
echo "所有参数: $@"
# 执行ls命令
ls
echo "ls命令退出状态: $?"
数组
说实话,Bash的数组用法挺独特:
# 定义数组
fruits=("apple" "banana" "orange")
# 访问元素
echo ${fruits[0]} # apple
echo ${fruits[@]} # 所有元素
echo ${#fruits[@]} # 数组长度
# 添加元素
fruits+=("grape")
# 切片
echo ${fruits[@]:1:2} # banana orange
第二部分:控制流
if语句
if [ condition ]; then
# 代码
elif [ condition ]; then
# 代码
else
# 代码
fi
条件测试的写法有点特别:
# 数值比较
if [ "$age" -eq 25 ]; then
echo "Age is 25"
fi
# 字符串比较
if [ "$name" = "Alice" ]; then
echo "Hello Alice!"
fi
# 文件测试
if [ -f "file.txt" ]; then
echo "file.txt exists"
fi
常用条件测试
数值比较:
-eq
等于-ne
不等于-gt
大于-lt
小于-ge
大于等于-le
小于等于
字符串比较:
=
相等!=
不等-z
空字符串-n
非空字符串
文件测试:
-f
常规文件-d
目录-r
可读-w
可写-x
可执行
case语句
有时候if-elif写多了看着烦,case语句更清晰:
case "$fruit" in
"apple")
echo "Selected apple"
;;
"banana"|"orange")
echo "Selected banana or orange"
;;
*)
echo "Unknown fruit"
;;
esac
循环
for循环
# 基础for循环
for i in 1 2 3 4 5; do
echo $i
done
# 序列
for i in {1..5}; do
echo $i
done
# C风格
for ((i=0; i<5; i++)); do
echo $i
done
# 遍历数组
fruits=("apple" "banana" "orange")
for fruit in "${fruits[@]}"; do
echo $fruit
done
# 遍历文件
for file in *.txt; do
echo "Processing $file"
done
while循环
# 基础while循环
count=1
while [ $count -le 5 ]; do
echo $count
((count++))
done
# 读取文件
while read line; do
echo "Line: $line"
done < input.txt
第三部分:函数
基础函数
# 函数定义
say_hello() {
echo "Hello, $1!"
}
# 调用函数
say_hello "Alice"
# 返回值
get_sum() {
local num1=$1
local num2=$2
echo $((num1 + num2))
}
result=$(get_sum 5 3)
echo "Sum: $result"
函数注意事项
- 函数必须先定义后使用
- return只能返回0-255的数字,要返回其他值用echo
- 函数内的变量默认是全局的,用local声明局部变量
函数库
实战中经常把常用函数放到单独的文件里:
# lib.sh
log_message() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# main.sh
source ./lib.sh
log_message "Starting script..."
第四部分:文本处理
重定向和管道
# 输出重定向
echo "Hello" > output.txt # 覆盖
echo "World" >> output.txt # 追加
# 错误重定向
ls nonexistent 2> error.log
# 同时重定向
command > output.txt 2>&1
# 管道
cat file.txt | grep "pattern" | sort
sed和awk
这两个命令堪称文本处理神器:
sed例子
# 替换
sed 's/old/new/' file.txt # 替换每行第一次匹配
sed 's/old/new/g' file.txt # 替换所有匹配
# 删除行
sed '3d' file.txt # 删除第3行
sed '/pattern/d' file.txt # 删除匹配行
# 插入和追加
sed '2i\new line' file.txt # 在第2行前插入
sed '2a\new line' file.txt # 在第2行后追加
awk例子
# 打印特定列
awk '{print $1}' file.txt # 打印第一列
awk '{print $1,$3}' file.txt # 打印第一、三列
# 条件处理
awk '$3 > 50 {print $1,$3}' data.txt # 打印第三列大于50的行的第一列和第三列
# 统计
awk '{sum += $1} END {print sum}' numbers.txt # 计算第一列之和
第五部分:实战示例
1. 系统监控脚本
#!/bin/bash
log_file="system_monitor.log"
monitor_system() {
# CPU使用率
cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}')
# 内存使用
memory_usage=$(free -m | awk 'NR==2{printf "%.2f%%", $3*100/$2}')
# 磁盘使用
disk_usage=$(df -h / | awk 'NR==2{print $5}')
# 写入日志
echo "[$(date '+%Y-%m-%d %H:%M:%S')] CPU: ${cpu_usage}% MEM: ${memory_usage} DISK: ${disk_usage}" >> "$log_file"
}
# 每分钟运行一次
while true; do
monitor_system
sleep 60
done
2. 批量文件处理
#!/bin/bash
# 配置
source_dir="./source"
backup_dir="./backup"
processed_dir="./processed"
# 创建必要的目录
mkdir -p "$backup_dir" "$processed_dir"
# 处理文件函数
process_file() {
local file="$1"
local filename=$(basename "$file")
# 备份
cp "$file" "$backup_dir/"
# 处理文件(示例:转换为小写)
tr '[:upper:]' '[:lower:]' < "$file" > "$processed_dir/$filename"
echo "Processed: $filename"
}
# 主处理循环
for file in "$source_dir"/*.txt; do
if [ -f "$file" ]; then
process_file "$file"
fi
done
3. 日志分析器
#!/bin/bash
# 配置
log_file="/var/log/apache2/access.log"
output_file="log_analysis.txt"
# 清空输出文件
> "$output_file"
# 分析函数
analyze_logs() {
echo "=== Log Analysis Report ===" >> "$output_file"
echo "Generated at: $(date)" >> "$output_file"
echo "" >> "$output_file"
# 访问量最大的IP
echo "Top 5 IP Addresses:" >> "$output_file"
awk '{print $1}' "$log_file" | sort | uniq -c | sort -nr | head -n 5 >> "$output_file"
echo "" >> "$output_file"
# 最常访问的页面
echo "Top 5 Requested Pages:" >> "$output_file"
awk '{print $7}' "$log_file" | sort | uniq -c | sort -nr | head -n 5 >> "$output_file"
echo "" >> "$output_file"
# HTTP状态码统计
echo "HTTP Status Code Distribution:" >> "$output_file"
awk '{print $9}' "$log_file" | sort | uniq -c | sort -nr >> "$output_file"
}
# 运行分析
analyze_logs
echo "Analysis complete. Results saved to $output_file"
第六部分:高级主题
错误处理
#!/bin/bash
# 设置错误处理
set -e # 遇到错误立即退出
set -u # 使用未定义变量时报错
set -o pipefail # 管道中的错误也会导致脚本退出
# 错误处理函数
error_handler() {
local line_num=$1
local error_code=$2
echo "Error on line $line_num: Exit code $error_code"
# 清理工作
cleanup
exit $error_code
}
# 设置错误处理器
trap 'error_handler ${LINENO} $?' ERR
# 清理函数
cleanup() {
echo "Performing cleanup..."
# 清理临时文件等
}
# 确保脚本结束时调用清理函数
trap cleanup EXIT
并发处理
#!/bin/bash
# 最大并发数
max_jobs=4
# 等待所有后台任务完成
wait_jobs() {
while [ $(jobs -p | wc -l) -ge "$max_jobs" ]; do
wait -n
done
}
# 处理函数
process_item() {
local item=$1
echo "Processing $item..."
sleep 2 # 模拟工作
echo "Finished $item"
}
# 主循环
for item in item{1..10}; do
wait_jobs # 控制并发数
process_item "$item" &
done
# 等待所有任务完成
wait
调试技巧
#!/bin/bash
# 启用调试模式
set -x # 打印执行的命令
set -v # 打印输入行
debug() {
[ "$DEBUG" = "true" ] && echo "DEBUG: $*"
}
# 使用调试函数
DEBUG=true
debug "Starting process..."
# 查看变量
declare -p variable_name
# 使用PS4自定义调试输出
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
最佳实践总结
脚本结构
#!/bin/bash # 常量定义 readonly CONFIG_FILE="/etc/myapp.conf" readonly MAX_RETRIES=3 # 函数定义 main() { parse_args "$@" setup_environment process_data cleanup } # 调用主函数 main "$@"
命名规范
# 变量名使用小写字母和下划线 user_name="Alice" # 常量使用大写字母 readonly MAX_CONNECTIONS=100 # 函数名使用小写字母和下划线 parse_arguments() { # ... }
安全注意事项
# 引用变量时总是使用双引号 file_name="my file.txt" if [ -f "$file_name" ]; then # 正确 rm "$file_name" fi # 设置安全的umask umask 077 # 使用mktemp创建临时文件 temp_file=$(mktemp) # 安全地处理密码 read -s -p "Password: " password
第七部分:常见问题和解决方案
1. 文件路径问题
说实话,这个坑我没少踩...
#!/bin/bash
# 获取脚本所在目录(这个技巧很有用)
script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# 使用绝对路径
config_file="${script_dir}/config.ini"
# 处理带空格的文件名
find . -type f -name "* *" | while IFS= read -r file; do
mv "$file" "${file// /_}"
done
2. 进程管理
#!/bin/bash
# 启动后台进程并保存PID
background_process() {
while true; do
echo "Working..."
sleep 1
done
}
background_process &
pid=$!
# 检查进程是否在运行
if kill -0 $pid 2>/dev/null; then
echo "Process is running"
fi
# 优雅地终止进程
cleanup() {
echo "Terminating background process..."
kill $pid
wait $pid
}
trap cleanup SIGTERM SIGINT
3. 性能优化
讲真,我觉得这部分特别重要:
#!/bin/bash
# 使用内存而不是磁盘
temp_array=()
while IFS= read -r line; do
temp_array+=("$line")
done < input.txt
# 避免多次调用外部命令
file_count=$(find . -type f | wc -l) # 不好
file_count=$(find . -type f -printf '.' | wc -c) # 更好
# 使用内置命令
number=123
if [[ $number =~ ^[0-9]+$ ]]; then # 使用bash正则,比调用grep快
echo "Is number"
fi
第八部分:实用工具函数库
我平时经常用到的一些函数,存下来没准儿对你也有用:
#!/bin/bash
# 日志函数
log() {
local level=$1
shift
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $*"
}
# 检查命令是否存在
require_command() {
local cmd=$1
if ! command -v "$cmd" >/dev/null 2>&1; then
log ERROR "Required command not found: $cmd"
exit 1
fi
}
# 版本比较
version_compare() {
if [[ $1 == $2 ]]; then
return 0
fi
local IFS=.
local i ver1=($1) ver2=($2)
for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)); do
ver1[i]=0
done
for ((i=0; i<${#ver1[@]}; i++)); do
if [[ -z ${ver2[i]} ]]; then
ver2[i]=0
fi
if ((10#${ver1[i]} > 10#${ver2[i]})); then
return 1
fi
if ((10#${ver1[i]} < 10#${ver2[i]})); then
return 2
fi
done
return 0
}
# 重试函数
retry() {
local attempts=$1
shift
local count=0
until "$@"; do
exit=$?
count=$((count + 1))
if [ $count -lt $attempts ]; then
log WARNING "Command failed. Attempt $count/$attempts. Retrying..."
sleep $((count * 2))
else
log ERROR "Command failed after $attempts attempts"
return $exit
fi
done
return 0
}
第九部分:常用代码片段
1. 配置文件处理
#!/bin/bash
# INI格式配置文件读取
get_ini_value() {
local file=$1
local section=$2
local key=$3
local value=$(awk -F '=' '/^\['"$section"'\]/{p=1;next} /^\[/{p=0} p&&$1~/"$key"/{print $2}' "$file")
echo "$value"
}
# JSON解析(使用jq)
parse_json() {
local file=$1
local key=$2
require_command jq
jq -r "$key" "$file"
}
2. 网络操作
#!/bin/bash
# HTTP请求
make_request() {
local url=$1
local method=${2:-GET}
local data=$3
if [ "$method" = "POST" ]; then
curl -X POST -H "Content-Type: application/json" -d "$data" "$url"
else
curl -s "$url"
fi
}
# 端口检查
wait_for_port() {
local host=$1
local port=$2
local timeout=${3:-30}
local start_time=$(date +%s)
while ! nc -z "$host" "$port" >/dev/null 2>&1; do
if [ $(($(date +%s) - start_time)) -gt "$timeout" ]; then
return 1
fi
sleep 1
done
return 0
}
3. 数据处理
#!/bin/bash
# CSV处理
process_csv() {
local file=$1
local column=$2
local operation=$3
case "$operation" in
sum)
awk -F',' "{sum+=\$$column} END {print sum}" "$file"
;;
average)
awk -F',' "{sum+=\$$column; count++} END {print sum/count}" "$file"
;;
*)
echo "Unknown operation: $operation"
return 1
;;
esac
}
# 数组操作
array_contains() {
local array=("$@")
local seeking=$1
shift
local element
for element in "${array[@]}"; do
[[ "$element" == "$seeking" ]] && return 0
done
return 1
}
结语
差点忘了说,这些例子都是我为了写这篇文章整理出来的。不敢说最佳实践,但起码是经过一些实战检验的。