一、前置概念

i. shell 兼容性

POSIX-conforming shell、Bourne-compatible shell (Bourne Shell 即 sh)、Unix shell

  • Unix shell:最宽泛的概念,泛指 Unix 或类 Unix 系统中的命令行 shell,例如 Bourne shell、bashzshcshfish 等。
  • POSIX-conforming shell:shell 的实现或某种运行模式符合 POSIX Shell Command Language 的要求。例如,bash 的默认模式包含超出 POSIX 的扩展,并在一些行为上有所不同,而 bash --posix 会使其行为更严格地遵循 POSIX。
  • Bourne-compatible shell:非正式地指在语法和基本行为上兼容传统 Bourne shell 的 shell;“兼容”到什么程度并没有统一的正式判定标准。它与 POSIX-conforming 不等价:传统 Bourne shell 早于 POSIX,Bourne-compatible shell 也可能包含扩展或存在不符合 POSIX 的行为;反过来,POSIX 规定的是可检验的标准行为,并不要求实现源自 Bourne shell。现代系统中的 sh 是命令名或系统接口,实际实现可能是 dashbash 等,也不等同于历史上的 Bourne shell。

ii. 正则表达式

正则表达式、正则 (Regular Expression, Regexp, Regex, RE),是一个用于字符串模式匹配的表述规则,在各个编程语言以及各个软件中都十分常用,大都类似但有细微区别。POSIX 有 Basic RE (BRE) 和 Extended RE (ERE) 两套标准,称为 POSIX BRE / POSIX ERE;并且它们都有对应的 GNU 扩展,称为 GNU BRE、GNU ERE;GNU Emacs 有一套标准;现代很多正则会采用类似 PCRE (Perl Compatible Regular Expressions) 的标准,或其相关的各种兼容性衍生标准 (Python, Java, JS 等)。

功能丰富度/易用度大体可以视为 PCRE > GNU Emacs > ERE >= BRE。

如下是学习网站。

在 POSIX BRE 中,分组和区间表达式分别写作 "\(...\)""\{m,n\}",不转义则视为文本匹配,例如 "a{2}" 匹配字面文本 "a{2}",而 "a\{2\}" 才能匹配 "aa"。不过字符组 "[...]"".""*" 则直接作为元字符使用:"[...]" 匹配其中列出的任意一个字符,"." 匹配任意单个字符,"*" 表示前一个表达式重复零次或多次。POSIX BRE 没有规定标准的 "+""?""|" 操作符,不过 GNU BRE 会额外提供 "\+""\?""\|" 用来表示重复一次或多次、重复零次或一次以及二选一。

在 POSIX ERE / GNU ERE 中则更加方便,标准语法规定了这些操作符,直接写作 "+""?""|" 即可,分组与区间表达式也可直接写作 "(...)""{m,n}"

这里用一个表格展示一下区别。

下表比较的是正则模式本身的语法与能力;其中 GNU BRE、GNU ERE 以 GNU 版本 grep 参数为准,PCRE 指当前使用的 PCRE2。 表示没有对应的标准或原生语法,并不表示一定无法用其他写法实现相同效果。

特性 POSIX BRE GNU BRE POSIX ERE GNU ERE GNU Emacs PCRE Python re JS RegExp Java regex
定位 POSIX 标准 POSIX BRE + GNU 扩展 POSIX 标准 POSIX ERE + GNU 扩展 Emacs 自有语法 PCRE2 语法 Python 标准库 ECMAScript 标准 java.util.regex.Pattern
匹配选择 最左、最长 最左、最长 最左、最长 最左、最长 最左、首个成功 最左、首个成功 最左、首个成功 最左、首个成功 最左、首个成功
任意单字符 . . . . . . . . .
. 匹配换行 取决于工具或 REG_NEWLINE 默认否;GNU grep -z 时可以 取决于工具或 REG_NEWLINE 默认否;GNU grep -z 时可以 默认否;DOTALL / (?s) 时可以 默认否;re.DOTALL / (?s) 时可以 默认否;s 标志时可以 默认否;Pattern.DOTALL / (?s) 时可以
行首与行尾 ^$ ^$ ^$ ^$ ^$ ^$ ^$ ^$ ^$
捕获分组 \(...\) \(...\) (...) (...) \(...\) (...) (...) (...) (...)
非捕获分组 \(?:...) (?:...) (?:...) (?:...) (?:...)
二选一 \| | | \| | | | |
重复零次或多次 * * * * * * * * *
重复一次或多次 \{1,\} \+ + + + + + + +
重复零次或一次 \{0,1\} \? ? ? ? ? ? ? ?
区间重复 \{m,n\} \{m,n\} {m,n} {m,n} \{m,n\} {m,n} {m,n} {m,n} {m,n}
懒惰重复 *?+??? *?+??? *?+??? *?+??? *?+???
占有重复 *+++?+ *+++?+ (3.11+) *+++?+
字符组与反选 [abc][^abc] [abc][^abc] [abc][^abc] [abc][^abc] [abc][^abc] [abc][^abc] [abc][^abc] [abc][^abc] [abc][^abc]
POSIX [[:name:]] 字符类写法 支持,如 [[:digit:]] 支持,如 [[:digit:]] 支持,如 [[:digit:]] 支持,如 [[:digit:]] 支持,如 [[:digit:]] 支持,如 [[:digit:]] 不支持;需按类别改写为普通字符组或相应简写 不支持;需按类别改用普通字符组、相应简写或 \p{...} (u/v 模式) 不支持 [[:name:]];使用 \p{Name},如 \p{Digit}
字符类简写 \s\S\w\W \s\S\w\W \w\W;另有参数化的 \sC\SC \d\D\h\H\N\s\S\v\V\w\W \d\D\s\S\w\W \d\D\s\S\w\W \d\D\h\H\s\S\v\V\w\W
编号反向引用 \1\9 \1\9 \1\9 \1\9 \1\9;也可用无歧义的 \g{1}\g{65535} \1\99 \1 起,可引用任意实际存在的编号 \1 起,可引用任意实际存在的编号
命名分组与引用 (?<n>...)\k<n> (?P<n>...)(?P=n) (?<n>...)\k<n> (?<n>...)\k<n>
单词边界 \b\B\<\> \b\B\<\> \b\B\<\> \b\B \b\B \b\B \b\B
前瞻 (?=...)(?!...) (?=...)(?!...) (?=...)(?!...) (?=...)(?!...)
后顾 (?<=...)(?<!...) (?<=...)(?<!...) (通常要求定长) (?<=...)(?<!...) (?<=...)(?<!...)
原子分组 (?>...) (?>...) (3.11+) (?>...)
Unicode 属性 — (另有 Emacs 字符类别) \p{...}\P{...} — (\d\w 默认支持 Unicode) \p{...}\P{...} (u/v) \p{...}\P{...}
条件分支 (?(条件)是/否) (?(组号/名称)是/否)
递归与子程序 (?R)(?1)(?&name)

官方参考:POSIX BRE/EREGNU grepGNU EmacsPCRE2Python reECMAScript RegExpJava Pattern

iii. 通配符

通配符 (wildcard) 不是正则表达式。尽管它也使用 ? * [] 这些和正则很类似的符号,但它和正则的含义不同。

具体可以参考 GNU/Linux Command-Line Tools Summary: Wildcards

例如,

  • . 是一个正则元符号,但不是一个通配符。
  • * 在正则中代表将前一个符号重复 >=0 次;而在通配符中代表 >=0 长度的任何符号,同时不匹配 "/"。粗略类似正则的 ".*" (注意 ".*" 一般不匹配换行,虽然通配符中一般也没有换行),如果匹配 "/" + 考虑换行可以类似正则的 "[[:print:]]*" (POSIX BRE、GNU BRE、POSIX ERE、GNU ERE、PCRE) 或 "(\S|\s)*" (GNU ERE、PCRE)、"[\S\s]*" (PCRE)。不匹配 "/" 则类似 "[^/]*"
  • ? 在 POSIX ERE、GNU ERE 和 PCRE 等正则语法中代表将前一个符号重复 0/1 次;而在通配符中代表任何一个单独字符,同时不匹配 "/""."。粗略类似正则的 ".",同样如果匹配 "/""." + 考虑换行可以类似正则的 "[[:print:]]" (POSIX BRE、GNU BRE、POSIX ERE、GNU ERE、PCRE) 或 "(\S|\s)" (GNU ERE、PCRE)、"[\S\s]" (PCRE)。不匹配 "/""." 则类似 "[^./]"
  • [] 和正则几乎相同。但是反选形如 [!abc],而正则形如 [^abc]

二、搜索处理相关命令

主要介绍 grepfindsedawk

i. grep 查找文件内容

POSIX grep(以及共有的非 POSIX 扩展)

匹配部分文本内容。默认使用 POSIX BRE 规则。

# 文件形式
grep "POSIX BRE 规则" "文件1" "文件2" ... "文件n"

# 文件夹形式(共有的非 POSIX 扩展;依次打开匹配,并非匹配文件名)
grep -r "POSIX BRE 规则" "文件夹1" "文件夹2" ... "文件夹n"  # -r / -R / --recursive

# 管道形式
(standard input pipe) | grep "POSIX BRE 规则"  # 例如
ls -lah | grep "\.py"

-i 无视大小写。

注意以上内容均是匹配文件内容(而非文件名),如果匹配文件名,一个做法是使用 ls 列举文件并从管道交给 grep,但更建议使用后文的 find

POSIX grep 默认使用 POSIX BRE;如果想要使用 POSIX ERE 或纯文本,可使用 -E-F

# POSIX ERE
grep -E "POSIX ERE 规则" "文件1" "文件2" ... "文件n"

# 纯文本
grep -F "纯文本" "文件1" "文件2" ... "文件n"  # 或传统的 fgrep

GNU 扩展 grep

GNU grep 中均使用 GNU BRE/ERE 规则,且 GNU grep 可通过 -P 使用 PCRE,

grep -P "PCRE 规则" "文件1" "文件2" ... "文件n"  # -P / --perl-regexp

GNU grep 同时可以用 rgrep 代替 grep -r(本身也有 -r),

rgrep "GNU BRE 规则" "文件夹1" "文件夹2" ... "文件夹n"

类似地,GNU grep 同时可以用 egrep 代替 grep -E(本身也有 -E),但 egrep 已弃用,现代脚本应优先使用 grep -E

egrep "GNU ERE 规则" "文件1" "文件2" ... "文件n"

其他选项可使用 man grep 自行阅读。

ii. find 查找文件名

POSIX find(以及共有的非 POSIX 扩展)

匹配整体文件名,默认通配符,

# 列举所有文件
find "文件夹1" "文件夹2" ... "文件夹n"

# 匹配
find "文件夹1" "文件夹2" ... "文件夹n" -name "通配符"

-iname 无视大小写。

grep "pattern" "file" ... 不同的是,findfind "dir" ... -name "pattern",pattern 是一个 key-arg 而不是 position-arg,并且 -name 等其他过滤条件要放在最后(其语法规定)。

find 同时支持对规则的否定、合取、析取,并使用转义的括号包裹(当然也要放最后)

# 举例,-not 可以用 \! 替代
find "文件夹1" "文件夹2" ... "文件夹n" \( -name "通配符1" -or -name "通配符2" \) -and -not -user tom

-regex-iregex 是共有的非 POSIX 扩展,匹配整体路径名而非整体文件名,单个文件甚至会包括开头的 ./。具体正则语法取决于实现。

find "文件夹1" "文件夹2" ... "文件夹n" -regex "正则规则"

-iregex 可以无视大小写。

-maxdepth 深度-mindepth 深度 也是共有的非 POSIX 扩展。注意请不要用 -depth 代替,它的意思是由深处开始向上搜索(先搜叶子节点再搜亲节点)

# 固定深度
find "文件夹1" "文件夹2" ... "文件夹n" -name "通配符" -maxdepth 3 -mindepth 2

GNU 扩展 find

GNU find-regex 默认使用近似 GNU Emacs 的正则语法,

find "文件夹1" "文件夹2" ... "文件夹n" -regex "近似 GNU Emacs 规则"

如果想要使用其他规则,需要额外指定 GNU 扩展 -regextype,根据手册使用 find -regextype help 查看有如下几种

'findutils-default', 'awk', 'egrep', 'ed', 'emacs', 'gnu-awk', 'grep', 'posix-awk', 'posix-basic', 'posix-egrep', 'posix-extended', 'posix-minimal-basic', 'sed'
# 其他规则
find "文件夹1" "文件夹2" ... "文件夹n" -regextype "规则类型" -regex "正则规则"

# 默认规则
find "文件夹1" "文件夹2" ... "文件夹n" -regextype findutils-default -regex "近似 GNU Emacs 规则"

# 其他规则
find "文件夹1" "文件夹2" ... "文件夹n" -regextype posix-minimal-basic -regex "近似 POSIX BRE 规则(最小规则集)"
find "文件夹1" "文件夹2" ... "文件夹n" -regextype posix-basic -regex "近似 POSIX BRE 规则"
find "文件夹1" "文件夹2" ... "文件夹n" -regextype ed -regex "近似 POSIX BRE 规则"
find "文件夹1" "文件夹2" ... "文件夹n" -regextype sed -regex "近似 POSIX BRE 规则"

find "文件夹1" "文件夹2" ... "文件夹n" -regextype grep -regex "GNU BRE 规则"

find "文件夹1" "文件夹2" ... "文件夹n" -regextype posix-extended -regex "POSIX ERE 规则"
find "文件夹1" "文件夹2" ... "文件夹n" -regextype awk -regex "近似 POSIX ERE 规则"
find "文件夹1" "文件夹2" ... "文件夹n" -regextype posix-awk -regex "近似 POSIX ERE 规则"

find "文件夹1" "文件夹2" ... "文件夹n" -regextype egrep -regex "GNU ERE 规则"
find "文件夹1" "文件夹2" ... "文件夹n" -regextype posix-egrep -regex "GNU ERE 规则"
find "文件夹1" "文件夹2" ... "文件夹n" -regextype gnu-awk -regex "近似 GNU ERE 规则"

find "文件夹1" "文件夹2" ... "文件夹n" -regextype emacs -regex "近似 GNU Emacs 规则"

除此之外,find 还可以用其他性质进行搜索(例如时间、作者等)。可使用 man find 自行阅读。

iii. awk

POSIX awk(以及共有的非 POSIX 扩展)

awk 是按“记录—字段”处理文本的模式扫描与处理语言,适合筛选、分列、统计、分组和格式化输出。默认把输入的每一行视为一条记录,再按空白把记录拆成字段。基本形式为

awk 'pattern { action }' "文件1" "文件2" ... "文件n"
(standard input pipe) | awk 'pattern { action }'
awk -f "程序文件.awk" "文件1" "文件2" ... "文件n"

通常应使用 shell 单引号包裹 awk 程序,否则 $0$1 等可能先被 shell 展开。程序由若干条 pattern { action } 规则组成:省略 pattern 表示匹配每条记录,省略 action 则等价于 { print $0 }

# 输出全部记录
awk '{ print }' "文件"

# 使用 POSIX ERE 筛选记录;/regexp/ 等价于 $0 ~ /regexp/
awk '/ERROR|WARN/ { print NR ":" $0 }' "文件"

# !~ 表示不匹配;next 立即处理下一条记录
awk '$0 !~ /^#/ { print; next }' "文件"

# 表达式也可以作为 pattern
awk '$3 >= 60 { print $1, $3 }' "文件"

# 从 start 到 stop 的范围 pattern,包含两端
awk '/start/,/stop/' "文件"

输入与输出的主要内置变量如下:

  • $0 是当前完整记录,$1$2……是各字段,$NF 是最后一个字段。
  • NF 是当前记录的字段数;NR 是所有输入累计的记录号;FNR 是当前文件内的记录号。
  • FILENAME 是当前文件名;ARGCARGV 保存命令行参数;ENVIRON 保存环境变量。
  • FSRS 是输入字段/记录分隔符;OFSORS 是输出字段/记录分隔符。

FS 默认值是一个空格,但它具有特殊含义:忽略记录首尾空白,并把连续的空格、制表符等视为一个分隔符。可以使用 -F 或在 BEGIN 中修改它;FS 是 POSIX ERE。

# 逗号分隔输入
awk -F ',' '{ print $1, $3 }' "文件"

# 输入以冒号分隔,输出以制表符分隔
awk -F ':' -v OFS='\t' '{ print $1, $3 }' "文件"

# BEGIN 在读取输入前执行,END 在全部输入处理完后执行
awk 'BEGIN { FS = ","; OFS = "\t" }
     NR > 1 { sum += $3; count++ }
     END { if (count) print "average", sum / count }' "文件"

# 每个文件的第一条记录;NR 不会在切换文件时归零,FNR 会
awk 'FNR == 1 { print "==> " FILENAME " <==" } { print }' "文件1" "文件2"

-v name=value 在执行 BEGIN 前赋值,适合把 shell 中的数据安全地作为普通值传入。直接放在文件参数之间的 name=value 则在处理到相应位置时才赋值。

limit=60
awk -v limit="$limit" '$3 >= limit { print $1, $3 }' "文件"

# 处理文件1后修改 FS,再处理文件2
awk '{ print NF, $0 }' FS=',' "文件1" FS=':' "文件2"

awk 支持数值、字符串、关联数组、函数、分支和循环。未初始化变量的数值为 0、字符串为空;关联数组的下标是字符串,使用 in 判断键是否存在,遍历顺序未规定。

# 分组计数
awk '{ count[$1]++ }
     END { for (key in count) print key, count[key] }' "文件"

# 去重并保留第一次出现的顺序
awk '!seen[$0]++' "文件"

# 自定义函数
awk 'function max(a, b) { return a > b ? a : b }
     { print max($1, $2) }' "文件"

常用字符串与输出函数包括:

  • length(s)substr(s, start, length)index(s, target)split(s, array, ERE)
  • sub(ERE, replacement, target) 替换第一个匹配,gsub(...) 替换全部匹配;省略 target 时修改 $0。二者返回替换次数。
  • match(s, ERE) 返回匹配起点,并设置 RSTARTRLENGTH
  • tolower(s)toupper(s)sprintf(format, ...)printf 按格式输出且不会自动换行。
# 替换每条记录中的全部连续空白
awk '{ gsub(/[[:space:]]+/, " "); print }' "文件"

# 固定宽度输出
awk '{ printf "%-20s %8.2f\n", $1, $2 }' "文件"

# split() 返回产生的元素个数
awk '{ n = split($0, part, ":"); print n, part[1] }' "文件"

正则常量写作 /POSIX ERE/,动态正则可以是字符串或变量。动态正则会先按 awk 字符串处理一次,再作为正则处理一次,因此反斜杠常需写两次;能使用正则常量时应优先使用正则常量。

awk '$1 ~ /^[[:alpha:]_][[:alnum:]_]*$/ { print $1 }' "文件"
awk -v pattern='^[[:digit:]]+$' '$1 ~ pattern { print }' "文件"

awk -F ',' 只是在逗号处分列,并不能正确解析字段内含逗号、引号或换行的通用 CSV 文件。

GNU 扩展 awk

GNU 实现的命令通常名为 gawkgawk --posix 禁用 GNU 和传统扩展并尽量严格遵循 POSIX,gawk --lint 报告可疑或不可移植的写法。默认正则在 POSIX ERE 的基础上还支持 \w\W\s\S\y\B\<\> 等 GNU 扩展;其他 GNU 正则通常用 \b 表示单词边界,但 awk 已将 \b 定义为退格,因此 gawk 改用 \y

gawk --posix 'program' "文件"
gawk --lint 'program' "文件"

常用 GNU 扩展包括:

  • IGNORECASE:非零时让正则匹配和部分字符串操作忽略大小写。
  • FPAT:描述“字段本身”而不是字段之间的分隔符;FIELDWIDTHS 按固定宽度分列。
  • BEGINFILEENDFILE:在每个文件开始和结束时执行;ARGIND 是当前文件在 ARGV 中的位置。
  • gensub():比 sub()gsub() 更灵活,可返回新字符串,并在 replacement 中使用 \1 等分组引用。
  • patsplit()、数组的数组、强类型正则 @/.../@include 和命名空间等语言扩展。
# 忽略大小写
gawk 'BEGIN { IGNORECASE = 1 } /error/ { print }' "文件"

# 每个文件分别输出开始和结束标记
gawk 'BEGINFILE { print "==> " FILENAME " <==" }
      { print }
      ENDFILE { print "==> end <==" }' "文件1" "文件2"

# 固定宽度字段:前 10、20、8 个字符
gawk 'BEGIN { FIELDWIDTHS = "10 20 8" }
      { print $1, $2, $3 }' "文件"

# 使用捕获分组交换两部分;原字符串不被修改
gawk '{ print gensub(/^([^:]+):(.*)$/, "\\2:\\1", 1) }' "文件"

GNU awk 可通过随发行版提供的 inplace 扩展原地编辑文件。建议指定备份后缀;这种方式会用新文件替换原文件,硬链接和符号链接语义需要特别注意。

gawk -i inplace -v inplace::suffix=.bak '{ gsub(/旧文本/, "新文本") } { print }' "文件1" "文件2"

参考:POSIX awkGNU awk 手册

iv. sed

POSIX sed(以及共有的非 POSIX 扩展)

sed 是流编辑器,适合按行选择、替换、删除、插入和重排文本。它默认把处理结果写到标准输出,不会直接修改输入文件

sed 'script' "文件1" "文件2" ... "文件n"
(standard input pipe) | sed 'script'
sed -e 'script1' -e 'script2' "文件"
sed -f "脚本文件.sed" "文件"

sed 按循环工作:读取一行到 pattern space,依次执行脚本,再默认输出 pattern space,然后开始下一轮。-n 关闭默认输出,此时需要使用 p 等命令显式输出。

# 默认输出全部输入;p 会再输出一次
sed 'p' "文件"

# 关闭默认输出,只打印选中的内容
sed -n 'p' "文件"
sed -n '/ERROR/p' "文件"

命令前可以放零个、一个或两个地址。地址可以是行号、$ 或正则;两个地址表示从第一个地址首次匹配处到随后第二个地址匹配处的闭区间。

# 第 5 行
sed -n '5p' "文件"

# 第 2 至第 5 行
sed -n '2,5p' "文件"

# 从 start 到 stop,包含两端
sed -n '/start/,/stop/p' "文件"

# 删除空行和以 # 开头的行
sed -e '/^[[:space:]]*$/d' -e '/^#/d' "文件"

# 地址后的 ! 表示对未匹配的记录执行命令
sed '/ERROR/!d' "文件"

最常用的 s 命令写作 s/regexp/replacement/flags。默认 regexp 使用 POSIX BRE;POSIX.1-2024 的 -E 选项切换为 POSIX ERE。分隔符不必是 /,处理路径时常换成 | 或其他字符以减少转义。

# 每行只替换第一个匹配
sed 's/旧文本/新文本/' "文件"

# g 替换每行的全部匹配
sed 's/旧文本/新文本/g' "文件"

# 使用其他分隔符
sed 's|/旧路径/|/新路径/|g' "文件"

# -E 使用 POSIX ERE;& 表示完整匹配,\1 表示第一个捕获分组
sed -E 's/^([^:]+):(.*)$/name=\1 value=\2/' "文件"

# 仅输出发生替换的行
sed -n 's/旧文本/新文本/gp' "文件"

常用命令可以按用途分为:

  • 输出与流程:p 输出 pattern space,d 删除并开始下一轮,q 退出;n 结束当前轮的默认输出(如果未使用 -n)并读取下一行继续执行。
  • 修改:s 替换,y/source/dest/ 逐字符转换,a/i/c 追加、插入、替换文本。
  • 多行处理:N 把下一行追加到 pattern space,P 输出到第一个换行为止,D 删除到第一个换行为止并重新开始脚本。
  • hold space:h/H 覆盖/追加保存,g/G 覆盖/追加取回,x 交换两个空间。
  • 分支::label 定义标签,b label 无条件跳转,t label 在最近一次替换成功时跳转。
  • 文件与信息:r file 读取文件,w file 写入文件,= 输出当前行号。

POSIX 可移植的 aic 写法应在命令后使用反斜杠和换行:

sed '1i\
标题
$a\
结尾' "文件"

pattern space 与 hold space 使 sed 可以跨行处理。N 会在两行之间保留换行,可在正则中用 \n 匹配这个内嵌换行。

# 把相邻两行中的 foo\nbar 合并为 foo bar
sed 'N; s/foo\nbar/foo bar/' "文件"

# 删除连续重复的相邻行
sed '$!N; /^\(.*\)\n\1$/!P; D' "文件"

复杂脚本应使用多行脚本或 -f,避免不同实现对分号、标签以及 a/i/c 文本语法的差异。

sed -n '
    /^#/d
    s/[[:space:]]*$//
    /./p
' "文件"

-i 是 GNU sed、BSD sed 等多个实现共有的非 POSIX 原地编辑扩展,但参数语法并不完全一致。建议指定备份后缀,并先在目标系统上确认写法;它会用新文件替换原文件,可能影响硬链接和符号链接。-u(减少缓冲)和作为 -E 别名的 -r 也由多个实现支持,但不是可移植的 POSIX 写法。

# GNU sed
sed -i.bak 's/旧文本/新文本/g' "文件"

# BSD/macOS sed
sed -i '.bak' 's/旧文本/新文本/g' "文件"

直接使用 sed -i 时不会经过“输出到临时文件后由用户确认”的步骤;脚本或正则写错可能立即改坏全部目标文件,应先不带 -i 检查输出,或至少使用备份后缀。

GNU 扩展 sed

GNU sed 常用扩展包括:

  • -z 以 NUL 而不是换行分隔记录,适合与 find -print0 等命令配合处理任意文件名。
  • -s 把多个输入文件分别处理,而不是视为一个连续输入。
  • --posix 禁用 GNU 扩展;--sandbox 禁止脚本执行 erw 等外部读写命令。
  • 地址 first~step0,/regexp/addr,+Naddr,~N
  • 正则修饰符 I(忽略大小写)和 M(多行模式),以及 GNU BRE 的 \+\?\|\w\b 等扩展。
  • replacement 中的 \L\U\l\u\E 大小写转换,以及 QRTWez 等扩展命令。
# 只处理第一次出现 regexp 之前的范围;0 地址是 GNU 扩展
sed '0,/regexp/s//replacement/' "文件"

# 从第 1 行开始,每隔 2 行执行一次(即奇数行)
sed '1~2s/^/[odd] /' "文件"

# 忽略大小写替换,并把完整匹配转换为大写
sed -E 's/[[:alpha:]]+/\U&/gI' "文件"

# 安全接收 find 输出中的任意文件名;输出仍以 NUL 分隔
find "文件夹" -print0 | sed -z 's|.*/||'

参考:POSIX sedGNU sed 手册

三、POSIX shell 相关命令

i.引号

单引号扩起的字符串不进行转义,而双引号会部分转义 + 参数 format

a="hello"
echo ${a}
# hello
echo "${a}"
# hello
echo '${a}'
# ${a}

ii.变量存在性

参考 shell 编程:冒号 后面跟 等号,加号,减号,问号的意义

echo ${a:-string}  # == ENV_DICT.get(a, "string")
echo ${a:=string}  # == ENV_DICT.setdefault(a, "string")
echo ${a:+string}  # == "string" if ENV_DICT.exist(a) else ""
echo ${a:?string}  # == try: ENV_DICT[a]; except: raise "string"

以上带冒号的形式同时测试参数是否未设置 (unset) 或为空 (null);去掉冒号的形式只测试是否未设置。

iii.变量修改

a="abcdefg-abcdefgh"

echo ${a}
# abcdefg-abcdefgh

截取(Bash 扩展功能)

echo ${a:2}  # 从n1开始截取
# cdefg-abcdefgh

echo ${a:2:7}  # 从n1开始截取n2长度
# cdefg-a

删除

# 与 ## 从前删除,% 与 %% 从后删除,## 和 %% 在使用 * 时进行最大匹配。

echo ${a#abc}
# defg-abcdefgh

echo ${a##abc}
# defg-abcdefgh

echo ${a#a*d}
# efg-abcdefgh

echo ${a##a*d}
# efgh