1. Shell编程入门:从基础到实战的20个经典案例
Shell脚本是Linux系统管理和自动化任务的利器,掌握它能让你的工作效率提升数倍。今天我将通过20个递进式实例,带你系统掌握Shell编程的核心技巧。这些案例都是我多年运维工作中反复验证过的实用代码,每个例子都包含基础版和进阶版,适合不同阶段的学习者。
提示:所有示例均在GNU Bash 5.0+环境测试通过,建议使用VS Code搭配ShellCheck插件编写脚本
1.1 基础输出与变量定义
1.1.1 基础变量与字符串拼接
bash复制#!/bin/bash
greeting="Hello"
name="World"
# 字符串拼接的三种经典方式
echo "${greeting}, ${name}!" # 推荐:清晰分隔
echo $greeting"_"$name # 无空格拼接
echo "${greeting}${name}2026" # 变量+固定字符
核心知识点:
- 变量定义时等号两侧不能有空格
${var}形式比$var更安全明确- 字符串拼接无需特殊符号,直接连接即可
运行结果:
code复制Hello, World!
Hello_World
HelloWorld2026
1.1.2 变量默认值与空值处理
bash复制#!/bin/bash
unset undefined_var
default_value="DEFAULT"
# 变量为空时使用默认值
echo "示例1: ${undefined_var:-$default_value}"
# 变量为空时赋值默认值并保存
echo "示例2: ${undefined_var:=$default_value}"
echo "当前值: $undefined_var"
# 检查变量是否设置
echo "示例3: ${undefined_var:+已设置}"
进阶技巧:
${var:-默认值}:空值时临时替换${var:=默认值}:空值时永久赋值${var:+已设置}:测试变量存在性${#var}:获取字符串长度${var%后缀}:截取字符串
1.2 用户输入处理技巧
1.2.1 基础输入与非空校验
bash复制#!/bin/bash
read -p "请输入用户名: " username
while [[ -z "$username" ]]; do
read -p "用户名不能为空,请重新输入: " username
done
echo "欢迎, $username!"
注意事项:
-z判断字符串是否为空[[ ]]比[ ]支持更多特性- 变量引用建议加双引号避免空格问题
1.2.2 密码输入与超时控制
bash复制#!/bin/bash
if read -t 10 -s -p "请在10秒内输入密码: " passwd; then
echo -e "\n验证中..."
# 实际场景这里应该是密码验证逻辑
echo "密码已接收"
else
echo -e "\n输入超时"
exit 1
fi
关键参数:
-t 10:设置10秒超时-s:隐藏输入内容$?获取上条命令退出状态
1.3 文件系统操作实战
1.3.1 文件类型判断
bash复制#!/bin/bash
file_path="/etc/passwd"
if [ -f "$file_path" ]; then
echo "常规文件: $file_path"
elif [ -d "$file_path" ]; then
echo "目录: $file_path"
else
echo "文件不存在或特殊类型"
fi
常用测试参数:
-f:常规文件-d:目录-e:存在即可-s:非空文件-r/-w/-x:权限检查
1.3.2 文件权限与大小检查
bash复制#!/bin/bash
check_file() {
local file=$1
[ -e "$file" ] || { echo "文件不存在"; return 1; }
echo "文件: $file"
echo "大小: $(du -h "$file" | cut -f1)"
echo "权限: $(stat -c "%A" "$file")"
echo "所有者: $(stat -c "%U" "$file")"
}
check_file "/var/log/syslog"
stat命令格式说明:
%A:人类可读权限%U:所有者用户名%s:字节大小%y:修改时间
1.4 数组操作全解析
1.4.1 数组遍历与长度
bash复制#!/bin/bash
fruits=("Apple" "Banana" "Orange" 123)
# 遍历数组
for fruit in "${fruits[@]}"; do
echo "水果: $fruit"
done
# 数组长度
echo "总数: ${#fruits[@]}"
echo "第一个元素长度: ${#fruits[0]}"
注意事项:
- 数组元素可以是不同类型
"${array[@]}"保留元素中的空格- 下标从0开始
1.4.2 数组切片与元素操作
bash复制#!/bin/bash
numbers=({1..10})
# 数组切片
echo "第2-5个元素: ${numbers[@]:1:4}"
# 元素替换
echo "替换后: ${numbers[@]/3/三}"
# 关联数组(需要Bash 4+)
declare -A user=([name]="John" [age]=30)
echo "用户名: ${user[name]}"
高级特性:
${array[@]:start:length}:数组切片${array[@]/pattern/repl}:元素替换declare -A:关联数组(类似字典)
1.5 循环控制实战
1.5.1 while循环数值计算
bash复制#!/bin/bash
count=1
max=5
while [ $count -le $max ]; do
echo "第$count次循环"
# 数学运算三种方式
((count++)) # 推荐
# count=$((count+1)) # 标准算术扩展
# let "count+=1" # let命令
done
数值比较运算符:
-eq:等于-ne:不等于-lt/-gt:小于/大于-le/-ge:小于等于/大于等于
1.5.2 while读取文件行
bash复制#!/bin/bash
while IFS= read -r line; do
# 跳过空行和注释
[[ -z "$line" || "$line" =~ ^# ]] && continue
echo "处理行: $line"
# 实际处理逻辑...
done < "/etc/passwd"
关键技巧:
IFS=防止前导/尾随空格被修剪-r禁止反斜杠转义=~正则匹配<输入重定向
1.6 函数编程技巧
1.6.1 函数参数与局部变量
bash复制#!/bin/bash
greet() {
local name=$1
local hour=$(date +%H)
if [ $hour -lt 12 ]; then
echo "早上好, $name"
else
echo "下午好, $name"
fi
}
greet "张三"
greet "李四"
最佳实践:
- 总是用
local声明局部变量 $1-$9获取位置参数$#获取参数个数$@获取所有参数
1.6.2 递归函数实现
bash复制#!/bin/bash
factorial() {
local n=$1
[ $n -le 1 ] && echo 1 && return
local prev=$(factorial $((n-1)))
echo $((n * prev))
}
echo "5的阶乘是: $(factorial 5)"
递归要点:
- 必须有终止条件
- 每次调用问题规模减小
- 注意栈溢出风险(深度限制)
1.7 命令行参数解析
1.7.1 基础参数处理
bash复制#!/bin/bash
echo "脚本名: $0"
echo "参数个数: $#"
echo "所有参数: $@"
for arg in "$@"; do
echo "参数: $arg"
done
# 使用示例: ./script.sh arg1 arg2
特殊变量:
$0:脚本名$1-$9:位置参数$#:参数个数$@:所有参数(保留分隔)$*:所有参数(单字符串)
1.7.2 高级选项解析(getopts)
bash复制#!/bin/bash
while getopts ":u:p:d" opt; do
case $opt in
u) user="$OPTARG" ;;
p) pass="$OPTARG" ;;
d) debug=true ;;
\?) echo "无效选项: -$OPTARG" >&2 ;;
esac
done
echo "用户: ${user:-未设置}"
echo "密码: ${pass:-未设置}"
echo "调试模式: ${debug:-false}"
# 使用示例: ./script.sh -u admin -p 123 -d
getopts说明:
:开头表示静默错误报告:在选项后表示需要参数$OPTARG获取选项参数$OPTIND存储下一个要处理的参数索引
1.8 文本处理三剑客
1.8.1 grep与awk处理CSV
bash复制#!/bin/bash
csv_data="id,name,age
1,John,30
2,Alice,25
3,Bob,35"
echo "年龄大于30的人:"
echo "$csv_data" | awk -F, 'NR>1 && $3>30 {print $2}'
echo "包含'li'的名字:"
echo "$csv_data" | grep -i "li" | cut -d, -f2
常用参数:
-F:指定字段分隔符NR:awk行号变量-i:grep忽略大小写cut -d:指定分隔符
1.8.2 sed流编辑器实战
bash复制#!/bin/bash
text="Hello World! Welcome to Shell Programming."
# 替换首次匹配
echo "${text/World/China}"
# 替换全部匹配
echo "${text//l/L}"
# 使用sed进行复杂替换
echo "$text" | sed -E 's/([A-Z])/[\1]/g'
sed技巧:
s/pattern/repl/:基本替换/g全局替换-E扩展正则\1反向引用
1.9 错误处理机制
1.9.1 严格模式与退出码
bash复制#!/bin/bash
set -euo pipefail
fail_cmd() {
echo "模拟失败..."
return 1
}
if ! fail_cmd; then
echo "命令失败,退出码: $?"
exit 1
fi
echo "这行不会执行"
严格模式:
set -e:错误立即退出set -u:未定义变量报错set -o pipefail:管道中任意错误都失败$?获取上条命令退出状态
1.9.2 信号捕获与日志记录
bash复制#!/bin/bash
trap 'echo "捕获中断信号,执行清理..."; exit' INT TERM
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> script.log
}
for i in {1..10}; do
log "处理第$i项"
sleep 1
done
常用信号:
INT:Ctrl+C中断TERM:终止信号EXIT:脚本退出DEBUG:每个命令前执行
1.10 批量文件处理
1.10.1 批量修改文件后缀
bash复制#!/bin/bash
# 将当前目录所有.txt改为.md
for file in *.txt; do
[ -e "$file" ] || continue # 处理无匹配情况
newname="${file%.txt}.md"
mv -v "$file" "$newname"
done
字符串操作:
${var%pattern}:删除后缀${var#pattern}:删除前缀${var/old/new}:替换首个${var//old/new}:替换全部
1.10.2 批量添加文件前缀
bash复制#!/bin/bash
prefix="backup_"
find . -maxdepth 1 -type f -name "*.conf" | while read -r file; do
dirname=$(dirname "$file")
basename=$(basename "$file")
mv -v "$file" "${dirname}/${prefix}${basename}"
done
find命令要点:
-maxdepth:搜索深度-type f:只找文件-name:名称模式while read处理含空格文件名
1.11 实战经验总结
- 变量引用安全:始终用
"${var}"形式,特别是处理文件路径时 - 错误处理前置:重要操作前检查依赖条件和权限
- 代码可读性:复杂逻辑添加注释,函数保持单一职责
- 性能考量:避免在循环中调用外部命令,使用内置字符串操作
- 兼容性注意:如果需要在不同Shell环境运行,避免使用Bash特有特性
调试技巧:
bash -x script.sh:跟踪执行set -x:开启调试trap 'echo $BASH_COMMAND' DEBUG:命令级调试shellcheck:静态分析工具
我在实际工作中发现,将常用功能封装成函数库能极大提升效率。例如创建一个~/.bash_functions文件存放你的常用函数,然后在.bashrc中添加source ~/.bash_functions,这些函数就能在所有脚本中使用了。