Bash 零基础教程

Bash 字符串处理

处理字符串是 Bash 脚本编写中极其基础且重要的一环。字符串本质上是字符的序列,操作它们可以让你处理文本数据、格式化终端输出以及构建高度动态的脚本。本章将全面涵盖基本的字符串操作,包括如何定义、访问、修改(截取/替换)以及比较字符串,为你提供在脚本中高效处理基于文本的数据所需的硬核技能。

1. 字符串字面量与变量

1.1 定义字符串字面量

在 Bash 中,字符串字面量是用单引号或双引号括起来的字符序列。

  • 单引号 ('...'): 严格保留引号内每个字符的字面值(所见即所得)。在单引号内,任何变量替换(如 $var)和命令执行(如 `date`)都不会发生。
string1='这是一个纯字符串'
echo $string1  # 输出: 这是一个纯字符串

string2='变量 x 的值是: $x'
echo $string2  # 输出: 变量 x 的值是: $x (注意:$x 没有被解析替换)

双引号 ("..."):允许进行变量替换和命令执行。诸如 $(用于标记变量)和反引号 `(用于命令替换)等特殊字符会被 Bash 解析并执行。

x=10
string3="变量 x 的值是: $x"
echo $string3  # 输出: 变量 x 的值是: 10

string4="今天是 $(date +%A)" # 推荐使用 $() 代替反引号
echo $string4  # 输出: 今天是 [星期几]

1.2 字符串赋值与访问

使用等号 = 将字符串字面量赋给变量。切记:等号周围不能有任何空格。

name="John Doe"
message="你好, $name!"
echo $message  # 输出: 你好, John Doe!

要访问字符串变量的值,请在变量名前加上 $,或者使用 ${variable_name}。后者在变量名紧接着其他字母或数字时非常有用,可以消除 Bash 解析变量名边界的歧义。

name="Jane"
echo $name       # 输出: Jane
echo ${name}Smith # 输出: JaneSmith (如果写成 $nameSmith,Bash 会去找名为 nameSmith 的变量)

2. 字符串高级操作

Bash 提供了一系列强大的内置参数扩展语法,让你无需调用诸如 awksed 等外部命令即可完成大多数日常字符串处理。

2.1 获取字符串长度

使用 ${#variable_name} 可以获取字符串的字符长度。

str="Hello"
length=${#str}
echo $length  # 输出: 5

2.2 子串提取 (切片)

你可以使用 ${variable_name:offset:length} 语法从一个长字符串中提取一段子串。

  • offset (偏移量): 子串的起始位置(索引从 0 开始计数)。
  • length (长度): 要提取的字符数量。如果省略此参数,将提取从 offset 一直到字符串末尾的所有内容。
str="Bash Scripting"
substring1=${str:0:4}  # 从索引 0 开始,提取 4 个字符
echo $substring1       # 输出: Bash

substring2=${str:5}    # 从索引 5 开始,提取到末尾
echo $substring2       # 输出: Scripting

substring3=${str:5:6}  # 从索引 5 开始,提取 6 个字符
echo $substring3       # 输出: Script

2.3 基于模式匹配删除子串 (掐头去尾)

Bash 提供了一种基于通配符模式,从字符串的开头(头部)或结尾(尾部)删除子串的巧妙方法,常用于处理文件路径和扩展名。

  • ${variable#pattern} (掐短头): 从字符串的开头删除与 pattern 匹配的最短部分。
  • ${variable##pattern} (掐长头): 从字符串的开头删除与 pattern 匹配的最长部分。
  • ${variable%pattern} (去短尾): 从字符串的结尾删除与 pattern 匹配的最短部分。
  • ${variable%%pattern} (去长尾): 从字符串的结尾删除与 pattern 匹配的最长部分。
file="path/to/my/file.txt.bak"

# 掐短头 (最短的前缀匹配)
short_prefix=${file#*/}
echo $short_prefix  # 输出: to/my/file.txt.bak (只删除了 path/)

# 掐长头 (最长的前缀匹配) -> 经典应用:提取纯文件名
long_prefix=${file##*/}
echo $long_prefix  # 输出: file.txt.bak (删除了 path/to/my/)

# 去短尾 (最短的后缀匹配) -> 经典应用:去除最后一个扩展名
short_suffix=${file%.*}
echo $short_suffix  # 输出: path/to/my/file.txt (只删除了 .bak)

# 去长尾 (最长的后缀匹配) -> 经典应用:去除所有扩展名
long_suffix=${file%%.*}
echo $long_suffix  # 输出: path/to/my/file (删除了 .txt.bak)

2.4 字符串替换

你可以使用参数扩展在字符串中替换特定的内容。

  • ${variable/pattern/replacement}: 将第一个匹配到的 pattern 替换为 replacement
  • ${variable//pattern/replacement}: 将所有匹配到的 pattern 替换为 replacement
str="apple banana apple"

# 替换第一个匹配项
replaced1=${str/apple/orange}
echo $replaced1  # 输出: orange banana apple

# 替换所有匹配项 (注意双斜杠 //)
replaced2=${str//apple/orange}
echo $replaced2  # 输出: orange banana orange

2.5 大小写转换

Bash 允许你轻松地转换字符串的大小写。
(注意:原生的大写 ^^ 和小写 ,, 语法仅在 Bash 4.0 或更高版本中可用。如果你使用的是极老的 macOS 默认 Bash,可能需要升级或使用备用方案。)

  • 转换为大写: ${variable^^}
  • 转换为小写: ${variable,,}
str="Hello World"

# 转换为大写
uppercase=${str^^}
echo $uppercase  # 输出: HELLO WORLD

# 转换为小写
lowercase=${str,,}
echo $lowercase  # 输出: hello world

备用方案 (适用于旧版 Bash):使用 tr 命令

str="Hello World"
uppercase=$(echo "$str" | tr '[:lower:]' '[:upper:]')
echo $uppercase # 输出: HELLO WORLD

3. 字符串对比

3.1 检查相等 (===)

在 Bash 的条件测试语句 [ ][[ ]] 中,单个等号 = 和双等号 == 都可用于检查两个字符串的内容是否完全相同。

str1="hello"
str2="world"

# 推荐使用双中括号 [[ ]]
if [[ "$str1" == "$str2" ]]; then
  echo "字符串相等"
else
  echo "字符串不相等"  # 输出: 字符串不相等
fi

3.2 检查不相等 (!=)

!= 运算符用于检查两个字符串是否不相等。

str1="hello"
str2="world"

if [[ "$str1" != "$str2" ]]; then
  echo "字符串确实不相等"  # 将会输出此行
fi

3.3 字典序/按字母顺序比较 (<, >)

你可以使用 <> 根据字母表的顺序(字典序)比较字符串。
极其重要的警告: 这与数字比较(-lt, -gt)的逻辑完全不同。

str1="apple"
str2="banana"

# 必须使用 [[ ]] 才能正确解析 < 和 > 作为比较符,而不是重定向符
if [[ "$str1" < "$str2" ]]; then
  echo "在字典中,apple 排在 banana 前面"  # 将会输出此行
fi

核心最佳实践提示:

  1. 在执行字符串比较时,务必用双引号把变量括起来(如 "$str1"),以防止当变量为空或包含空格时引发可怕的语法错误。
  2. 在现代 Bash 脚本中,进行字符串条件判断时,强烈推荐使用 [[ ]] 而不是旧式的 [ ][[ ]] 更安全,支持模式匹配,并且不需要对 <> 等符号进行繁琐的反斜杠转义。

4. 实战案例演示

案例 1:快速验证用户输入是否为空

在编写交互式脚本时,经常需要确保用户没有直接按回车跳过输入。我们可以使用 -z(Zero length,长度为零)或 -n(Non-zero length,长度非零)来快速检查。

read -p "请输入您的密码: " user_password

if [[ -z "$user_password" ]]; then
  echo "错误:密码不能为空!"
  exit 1
fi
echo "密码已接收。"

案例 2:使用正则表达式验证输入格式

如果你使用双中括号 [[ ]],你可以使用 =~ 运算符将字符串与正则表达式 (Regular Expressions) 进行匹配。这是极其强大的验证功能。

read -p "请输入一个合法的用户名 (仅限字母和数字): " username

# ^[a-zA-Z0-9]+$ 表示从头到尾只能由至少一个英文字母或数字组成
if [[ ! "$username" =~ ^[a-zA-Z0-9]+$ ]]; then
  echo "验证失败:用户名只能包含字母和数字组合。"
  exit 1
fi
echo "用户名 '$username' 格式验证通过。"

案例 3:批量修改文件扩展名

假设你想将当前目录下所有以 .old 结尾的文件批量重命名为 .new。这就用到了我们前面学到的“去尾”操作。

for file in *.old; do
  # 检查是否真的找到了文件(防止目录下没有 .old 文件时发生错误)
  if [[ -f "$file" ]]; then
    newfile="${file%.old}.new" # 核心操作:截掉 .old,拼上 .new
    mv "$file" "$newfile" 
    echo "已将文件重命名: $file -> $newfile"
  fi
done