Bash 语法补充包
目前版本:20221025
本文期望读者具有基本的 Bash 使用基础。如在阅读中遇到困难,可参阅我社 Linux 101 第六讲内容。
Bash 大致语法 Overview
使用 A::B 指示 "用 B 分隔的 A 列表"
nls: newlines, newline list, 指一个或多个 newline
sop: separator_op, 分割不同 bash 指令用的操作符, 可以是 '&' 或 ';'
complete_cmd : list sop?
list : and_or::sop
pipeline : "!"? cmd::"|"
cmd: simple_cmd | compound_cmd redirect* | func_def
compound_cmd
: '{' list '}' # brace group
| '(' list ')' # subshell
| for | case | if | while | until
redirect: 详见下面的章节
func_def: name '(' ')' compound_cmd redirect?
重定向相关 (Redirection)
参考:https://www.gnu.org/software/bash/manual/html_node/Redirections.html
重定向带顺序的!!! 由后往前执行!!!! 比如说 2>&1 >f 会先 >f, 然后才 2>&1, 导致无法产生想要的结果; 应该使用 >f 2>&1.
下面用 n, nn 代表数字 (文件描述符), m 代表数字或 -, f 代表文件名,T 代表任意文本,[] 中代表可选
[n]<f: 重定向输入n(=0)为文件f[n]>[|]f: 重定向输出n(=1)为文件f, 当启用了noclobber且有|时不会覆盖已存在的文件[n]>>f: 使用附加模式重定向输出,&>f,>&f: 重定向1和2到文件f, 优先选择第一种,等价于>f 2>&1(注意顺序)&>>f: 使用附加模式重定向1,2,等价于>>f 2>&1[n]<<[-]d ...TEXT d: here-document, 向n(=0)里送...TEXT, 带-则忽略...TEXT中的行首 Tab[n]<<< ...TEXT: here-string[n]<&m: 将n(=0)设置为m的复制; 若m=-, 则关闭n[n]>&m: 将m设置为n(=1)的复制; 若m=-, 则关闭n[n]<&nn-, 将nn"移动" 到n(=0)(也就是复制到,再关闭)[n]>&nn-, 将n(=1)"移动" 到nn
noclobber 可以通过如下方法启用:
set -o noclobber
echo Hello > file
echo Hello >| file # fail
一些特殊的文件:
/dev/null: 空文件,可以 "吞噬" 任何输入,一般用于丢弃输出/dev/random: 输出随机值的文件/dev/fd/n: 文件描述符n代表的文件/dev/stdin,/dev/stdout,/dev/stderr/dev/tcp/host/port: 建立 TCP 链接/dev/udp/host/port: 建立 UDP 链接
Bash 表达式展开过程
参考:https://www.gnu.org/software/bash/manual/html_node/Shell-Expansions.html
- 大括号展开 (brace expansion)
- 波浪号展开 (tilde expansion)
- 参数展开 (parameter expansion)
- 命令替换 (command substitution)
- 算术展开 (arithmetic expansion)
- 单词切分 (word splitting)
- 文件名展开 (filename expansion)
- (可选) 进程替换 (process substitution)
- 引号删除 (quote removal)
大括号展开
多选
类似文件名展开,但是文件名展开要求文件存在,而大括号展开不需要。
# 注意大括号展开的逗号后面不能加空格, 否则会失败
$ echo a{d,c,b}e
ade ace abe
大括号内还可以写数字/字符+增长, 比如 {x..y[..incr]}
大括号展开是严格 "词法" 的,它不会对任何特殊字符作出 "反应", 包括大括号自己:
chown root /usr/{ucb/{ex,edit},lib/{ex?.?*,how_ex}}
波浪号展开
快速访问目录栈与
HOME
~:$HOME~/foo:$HOME/foo~fred/foo: fred 的 HOME 下的 foo 文件夹~+/foo:$PWD/foo~-/foo:${OLDPWD-'~-'}/foo, 但是展开结果与$OLDPWD/foo很类似,推测大括号是用于删除可能的末尾斜杠的~N,~+N:dirs +N, 其实~+也可以看作N=0的情况~-N:dirs -N, 其实~-也可以看作N=1的情况
参数展开
${!p*},${!p@}: 间接展开,先展开名字前缀为p的变量,把展开后的东西当变量名再展开一次${p:-default}: 默认值${p:=default}: 空的时候不但用默认值代替,还会赋值${p:?prompt}: 空的时候往 stderr 打 prompt${p:+prompt}: 不空的时候往 stderr 打 prompt${p:offset},${p:offset:length}: 切片,从[offset, offset + lenght), 从 0 开始,负数反之。负数 offset 负号前要打空格,否则会跟:-冲突。如果 p 是@或是*, 那么对位置参数数组做切片${#p}: 获得 p 展开后的字符长度${p@op}: 对 p 展开后做一些变换:U: 转全大写u: 转首字母大写L: 转全小写Q: 转成 "quoted" 的形式,即还可以被 input 读回去的形式E: 展开后带一些反斜杠转义,使结果成为有效的变量名,可以被用于$'...'P: 展开后,使结果成为有效的 PromptA: 展开成p=$p的格式K: 与A类似,但是对数组会展开成键值对赋值的形式a: 展开成 p 的属性k: 与K类似,但是展开后的东西会过一边单词切分
还有一堆跟模式匹配有关的变量展开,标志是大括号里有 #%/^, 的
命令替换
$(command) 和 `command`. 特别地,$(cat file) 可以被替换为更快的 $(< file). 命令替换可以叠加,如果使用两个反引号引住命令替换,替换后的内容将不会经过内层的单词切分和文件名展开。
算术展开
$(( expr )). 里面的单词会经历参数展开,变量替换和引号消除,可以嵌套。合法的算式可以看这里 Shell Arithmetic
进程替换
<(cmd-list): 执行cmd-list, 其输出会被写入到某个文件里,然后这个文件的路径被当成展开结果>(cmd-list): 会被展开成一个文件路径,然后往该路径里的任何写入会被当成cmd-list的标准输入
参考:SO
常见使用:
# 这样不行, 因为 bash 的 stdin 是读给脚本当 stdin 的, 不是用来读脚本的
curl -o - http://example.com/script.sh | bash
# 这样可以, 下载下来的脚本被存到某个文件里, 然后 bash 可以接受一个文件名作为参数执行那个文件
bash <(curl -o - http://example.com/script.sh)
# 查找所有我不够权限看的文件
# 这里把标准错误写到了一个文件里, 这个文件里的内容会被当成 sed 的标准输入
(ls /proc/*/exe >/dev/null) 2> >(sed -n \
'/Permission denied/ s/.*\(\/proc.*\):.*/\1/p' > denied.txt )
单词切分
将环境变量 IFS 当作单词切分的分隔符,然后对新扩展的任何内容进行切分。需要注意的是,如果没有进行过扩展,那么单词切分也不会被进行。
When a quoted null argument appears as part of a word whose expansion is non-null, the null argument is removed.
这意味着 -d'' 并不会是 -d '', 而只是 -d.
文件名扩展
参见 glob.
引号移除
移除所有引号并进行转义。
完整 Posix Shell 语法
https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_10