王美洁

1.12 通配符、正则、脚本参数和字符切片

现在开始,我们开始讲讲如何提升效率,即批量操作,这是计算机最擅长的事情,也是人类最不擅长的事。

1. 通配符是什么

通配符主要是 shell 在匹配文件名和路径时使用的。

最常见的是:

  • *:匹配任意长度字符
  • ?:匹配单个字符
  • [abc]:匹配方括号中的任意一个字符
  • [0-9]:匹配一个范围内的字符

例如:

bash
ls *.vasp             # 列出当前目录下所有 .vasp 文件
cp */CONTCAR summary/ # 复制各子目录中的 CONTCAR  summary 目录
ls run?               # 匹配 run1 run2,但不匹配 run10
ls run[12]            # 匹配 run1  run2
ls file[0-9].txt      # 匹配 file0.txt  file9.txt

这里最重要的直觉是:

通配符通常是在匹配文件名和路径,不是在匹配文件内容。

所以:

  • *.txt 是在匹配文件名
  • job* 是在匹配以 job 开头的文件或目录
  • */OUTCAR 是在匹配各个子目录中的 OUTCAR
  • run[12] 是在匹配特定字符集合

2. 还有一类常见写法:shell 展开

除了严格意义上的通配符,你还会经常看到另一类写法:

  • {a,b}
  • {1..5}
  • ~

它们经常和通配符一起出现,所以新手很容易把它们统称成一类。

这样理解不算完全错,因为它们都属于 shell 帮你“展开”命令的一部分。

但更严格一点说:

  • *?[] 更偏向文件名匹配
  • {}~ 更偏向 shell 自己先做展开

你现在不用死抠术语边界,但要先知道:

它们都不是在匹配文件内容,而是在命令真正执行之前,由 shell 先展开。

最常见的几个例子:

bash
echo {a,b}        # 展开成 a b
echo {1..5}       # 展开成 1 2 3 4 5
cp report.{pdf,txt} backup/ # 展开成两个文件名
cd ~              # 展开成当前用户家目录

这里的:

  • {a,b}:多选展开
  • {1..5}:范围展开
  • ~:家目录展开

{1..5} 这类写法很重要,因为后面写循环或批量命令时非常常见。

例如:

bash
mkdir run{1..5} # 一次创建 run1  run5
cp input run{1..5}/ # 一次展开出多个目录参数

3. 通配符和 shell 展开最常见的场景

目录结构一旦统一,很多重复操作都可以一次做掉。

例如:

bash
cp *.txt backup/      # 复制所有 txt 文件
rm run*/WAVECAR       # 删除各个 run 目录里的 WAVECAR
mv *.log logs/        # 移动所有 log 文件到 logs 目录
mkdir calc_{a,b,c}    # 一次创建多个目录
mkdir run{1..5}       # 一次创建编号目录

所以通配符的价值,不是“语法炫技”,而是:

  • 少打字
  • 少重复
  • 统一命名后批量处理很方便

4. 正则表达式又是什么

正则表达式和通配符很像,但它主要是拿来匹配文本,不是拿来匹配文件路径。

你以后最常见它的地方包括:

  • grep
  • sed
  • awk
  • Python 里的 re

不需要一上来学完整正则系统,先知道几个最常见的符号就够了:

  • .:匹配任意单个字符
  • *:重复前一个模式 0 次或多次
  • ^:匹配行首
  • $:匹配行尾
  • [abc]:匹配方括号中的任意一个字符
  • [0-9]:匹配一个数字

例如:

bash
grep '^Step' log.txt    # 匹配以 Step 开头的行
grep 'error$' log.txt   # 匹配以 error 结尾的行
grep 'run[0-9]' log.txt # 匹配 run1 run2 这类模式
grep 'a.*b' log.txt     # 匹配 a  b 之间有任意内容的文本

5. shell 脚本参数

除了通配符和正则,你后面还会经常看到 shell 脚本里这种写法:

bash
$1
$2
$#
$@

这些不是通配符,也不是正则。

它们表示的是:你在运行脚本时传进去的参数。

例如有一个脚本 run.sh

bash
#!/bin/bash
echo $1
echo $2

如果你这样运行:

bash
bash run.sh file1 file2

那么:

  • $1 就是第一个参数 file1
  • $2 就是第二个参数 file2

另外 $# 表示总共传进来了多少个参数:

bash
#!/bin/bash
echo "number of args = $#"

$@ 表示所有参数:

bash
#!/bin/bash
echo "$@"

6. 字符串切片

后面写 shell 脚本时,你还会经常遇到一种需求:

  • 从文件名里截出一部分
  • 去掉某个后缀
  • 拼一个新的名字

例如:

bash
name="POSCAR.vasp"
echo "${name%.vasp}"      # 去掉后缀,得到 POSCAR
echo "${name%.vasp}.csv"  # 去掉后缀后再拼新后缀,得到 POSCAR.csv
echo "${name}.bak"        # 直接在原名字后面拼内容,得到 POSCAR.vasp.bak

这里看着是不是觉得有点多此一举?哈哈是的,如果是单个文件不需要这么操作,接下来我们讲讲循环,释放真正的力量。