1. Shell字符串基础与操作技巧
字符串处理是Shell脚本编程中最基础也最常用的功能之一。不同于其他编程语言,Shell中的字符串有其独特的特性和操作方法,这也是新手最容易踩坑的地方。
1.1 Shell字符串的定义与引号使用
在Shell中定义字符串有三种主要方式:
- 无引号:
str=hello - 单引号:
str='hello world' - 双引号:
str="hello $USER"
重要区别:单引号内的内容会原样输出,不进行变量替换或转义;双引号会解析变量和特殊字符;无引号字符串不能包含空格,且会进行通配符扩展。
实际应用中,我强烈建议始终使用引号包裹字符串,这可以避免90%的意外错误。特别是在处理文件路径时:
bash复制# 错误示范(路径中有空格会出问题)
rm /path with spaces/file.txt
# 正确做法
rm "/path with spaces/file.txt"
1.2 字符串拼接与长度获取
Shell中字符串拼接不需要特殊运算符,直接并列即可:
bash复制str1="Hello"
str2="World"
result=$str1$str2 # 输出HelloWorld
result="$str1 $str2" # 输出Hello World
获取字符串长度使用${#var}语法:
bash复制str="abcdefg"
echo ${#str} # 输出7
1.3 字符串截取与替换
Shell提供了强大的字符串操作功能:
bash复制str="abcdefghijk"
# 截取第3个字符开始的后5个字符
echo ${str:2:5} # 输出cdefg
# 删除最短匹配的前缀
echo ${str#a*c} # 输出defghijk
# 删除最长匹配的后缀
echo ${str%h*k} # 输出abcdefg
在实际脚本开发中,我经常用这些操作来处理文件名和路径:
bash复制filename="/var/log/app.log"
echo ${filename##*/} # 获取文件名:app.log
echo ${filename%.*} # 去掉扩展名:/var/log/app
1.4 字符串分割与数组转换
Shell没有内置的split函数,但可以通过设置IFS(内部字段分隔符)来实现:
bash复制str="apple,orange,banana"
IFS=',' read -ra arr <<< "$str"
echo ${arr[1]} # 输出orange
更复杂的场景下,可以使用awk:
bash复制echo "$str" | awk -F, '{print $2}' # 输出orange
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Shell运算符详解与应用
Shell支持多种运算符,包括算术、关系、布尔、字符串和文件测试运算符。正确理解和使用这些运算符是编写健壮脚本的关键。
2.1 算术运算符
Shell中算术运算有三种方式:
- 使用expr(老旧方式)
- 使用$(( ))语法(推荐)
- 使用let命令
bash复制# 基本算术运算
a=10
b=3
echo $((a + b)) # 13
echo $((a * b)) # 30
echo $((a % b)) # 1
# 自增运算
((a++)) # a变为11
特别注意:Shell默认所有变量都是字符串,所以进行算术运算必须使用上述方式,直接写
a+b会被当作字符串拼接。
2.2 关系运算符
关系运算符用于数值比较(使用-gt、-lt等)和字符串比较(使用=、!=等):
bash复制# 数值比较
if [ $a -gt $b ]; then
echo "$a大于$b"
fi
# 字符串比较
str1="hello"
str2="world"
if [ "$str1" != "$str2" ]; then
echo "字符串不相等"
fi
2.3 布尔运算符与逻辑运算符
Shell提供了-a(与)、-o(或)和!(非)等布尔运算符:
bash复制if [ $a -gt 5 -a $a -lt 15 ]; then
echo "$a在5和15之间"
fi
更现代的写法是使用&&和||:
bash复制if [[ $a > 5 && $a < 15 ]]; then
echo "$a在5和15之间"
fi
2.4 字符串运算符
专门用于字符串测试的运算符:
bash复制str="hello"
# 检查字符串是否为空
if [ -z "$str" ]; then
echo "字符串为空"
fi
# 检查字符串是否非空
if [ -n "$str" ]; then
echo "字符串非空"
fi
2.5 文件测试运算符
Shell提供了一系列文件测试运算符,这在脚本中非常实用:
bash复制file="/path/to/file"
if [ -f "$file" ]; then
echo "普通文件存在"
fi
if [ -d "$file" ]; then
echo "目录存在"
fi
if [ -x "$file" ]; then
echo "文件可执行"
fi
3. Shell内置命令深度解析
Shell内置命令是直接内建在Shell解释器中的命令,执行效率高且功能强大。理解这些命令对编写高效脚本至关重要。
3.1 常用内置命令概览
以下是最常用的Shell内置命令:
| 命令 | 功能描述 | 示例 |
|---|---|---|
| echo | 输出文本 | echo "Hello" |
| read | 读取输入 | read -p "Enter:" var |
| test/[ | 条件测试 | [ -f file ] |
| exec | 执行命令 | exec ls |
| shift | 参数左移 | shift 2 |
| exit | 退出脚本 | exit 1 |
| trap | 信号处理 | trap "handler" SIGINT |
3.2 read命令的高级用法
read命令远比看起来强大:
bash复制# 读取多个变量
read -p "Enter name and age: " name age
# 设置超时
if read -t 5 -p "Quick response: " answer; then
echo "You answered: $answer"
else
echo "Timeout!"
fi
# 读取密码(不显示输入)
read -s -p "Password: " pass
3.3 exec命令的妙用
exec命令可以替换当前进程或重定向文件描述符:
bash复制# 重定向脚本所有输出到文件
exec > script.log 2>&1
# 替换当前shell进程
exec /bin/bash
3.4 trap命令处理信号
trap命令可以捕获和处理信号:
bash复制cleanup() {
echo "Cleaning up..."
rm -f tempfile
}
trap cleanup EXIT INT TERM
3.5 shift处理命令行参数
shift命令在处理位置参数时非常有用:
bash复制while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
show_help
;;
-v|--verbose)
verbose=1
;;
*)
files+=("$1")
;;
esac
shift
done
4. Shell脚本中的常见陷阱与解决方案
在实际开发中,Shell脚本有许多容易踩坑的地方。这里分享一些我多年积累的经验和解决方案。
4.1 变量引用的坑
未引用的变量会导致各种奇怪问题:
bash复制filename="My Document.txt"
# 错误:会被当作两个参数
rm $filename
# 正确
rm "$filename"
经验法则:除非有特殊需求,否则总是用双引号包裹变量引用。
4.2 测试条件的常见错误
测试条件语法非常严格:
bash复制# 错误:等号两边无空格
if [ "$var"="value" ]; then
# 正确
if [ "$var" = "value" ]; then
# 更现代的写法(避免很多问题)
if [[ $var == value ]]; then
4.3 算术运算的精度问题
Shell默认只支持整数运算:
bash复制# 错误:不支持浮点数
result=$((5 / 2)) # 得到2
# 解决方案:使用bc
result=$(echo "scale=2; 5 / 2" | bc) # 得到2.50
4.4 管道中的变量作用域
管道会创建子shell,变量修改不会影响父shell:
bash复制count=0
cat file.txt | while read line; do
((count++))
done
echo $count # 输出0!
# 解决方案:使用进程替换
while read line; do
((count++))
done < <(cat file.txt)
echo $count # 正确计数
4.5 错误处理的最佳实践
默认情况下,Shell脚本会忽略错误继续执行:
bash复制# 错误:即使命令失败也会继续
rm non_existent_file
echo "继续执行"
# 正确做法:设置错误退出
set -euo pipefail
rm non_existent_file || {
echo "删除文件失败" >&2
exit 1
}
4.6 性能优化技巧
处理大文件时,Shell脚本可能很慢:
bash复制# 慢:多次调用grep
grep "error" log.txt | grep -v "warning"
# 快:使用单个awk命令
awk '/error/ && !/warning/' log.txt
另一个常见场景是字符串拼接:
bash复制# 慢:循环中反复拼接
result=""
for i in {1..1000}; do
result+="$i"
done
# 快:使用printf
printf -v result "%s" {1..1000}
5. 实战案例:构建健壮的Shell脚本
结合前面所学,我们来看一个综合案例:编写一个统计日志文件中错误信息的脚本。
5.1 需求分析
我们需要一个脚本能够:
- 接受日志文件路径作为参数
- 统计不同错误类型的出现次数
- 输出汇总报告
- 处理各种边界情况
5.2 完整实现代码
bash复制#!/bin/bash
set -euo pipefail
usage() {
echo "Usage: $0 [-v] [-h] <logfile>"
exit 1
}
# 初始化变量
verbose=0
logfile=""
# 解析参数
while getopts "vh" opt; do
case "$opt" in
v) verbose=1 ;;
h) usage ;;
*) usage ;;
esac
done
shift $((OPTIND-1))
[ $# -eq 1 ] || usage
logfile="$1"
# 检查文件存在且可读
[ -f "$logfile" ] && [ -r "$logfile" ] || {
echo "错误:无法读取文件 '$logfile'" >&2
exit 2
}
# 定义错误模式
declare -A error_patterns=(
["ERROR"]="ERROR|FAILED"
["WARNING"]="WARN|TIMEOUT"
["CRITICAL"]="CRITICAL|SEVERE"
)
# 统计错误
declare -A error_counts
for type in "${!error_patterns[@]}"; do
count=$(grep -Ec "${error_patterns[$type]}" "$logfile" || true)
error_counts[$type]=$count
[ "$verbose" -eq 1 ] && echo "调试:$type 计数=$count"
done
# 生成报告
echo "==== 错误统计报告 ===="
echo "分析文件: $logfile"
echo "分析时间: $(date)"
echo "------------------------"
for type in "${!error_counts[@]}"; do
printf "%-10s: %4d\n" "$type" "${error_counts[$type]}"
done
echo "========================"
exit 0
5.3 关键点解析
- 参数解析:使用getopts处理命令行选项,支持-v(详细输出)和-h(帮助)
- 错误处理:set -euo pipefail确保脚本在错误时退出,并检查文件可读性
- 关联数组:使用declare -A存储错误模式和计数
- 健壮性:处理grep可能返回非零的情况(|| true)
- 格式化输出:使用printf对齐输出结果
5.4 扩展建议
这个脚本还可以进一步扩展:
- 添加-t选项设置时间范围过滤
- 支持多个日志文件输入
- 生成HTML格式报告
- 添加邮件通知功能
在实际工作中,我发现这类脚本最常被用于监控系统日志和应用程序日志。掌握这些Shell编程技巧可以显著提高运维效率。
