fish-shell 中的 not 命令:翻转退出状态、`!` 别名与实现原理全解析
fish-shell 中的 not 命令:翻转退出状态、! 别名与实现原理全解析
本文以 fish-shell 官方命令手册 not.rst 为核心,系统讲解 not(及其等价别名 !)如何将一条命令或一个作业(job)的退出状态取反,包括语法格式、行为语义、if/and/or 等复合结构中的实战用法,并从 parser_keywords.rs、parse_execution.rs、proc.rs 等源码出发,剖析其作为保留关键字、AST 节点以及 JobFlags::negate 标志背后的完整实现链路。读完本文,你将掌握 not 的一切用法,并能理解它与 ! 别名、exec、管道、变量赋值组合时的真实行为。
概述:not 是什么
在 fish 中,not 是一个内建的保留关键字(reserved keyword),而不是一个普通的外部命令。它的作用只有一个:把紧跟在它后面的命令或作业的退出状态取反——命令成功(退出状态为 0)时 not 返回 1;命令失败(退出状态非 0)时 not 返回 0。
官方文档 not.rst 中的原话是:
notnegates the exit status of another command. If the exit status is zero,notreturns 1. Otherwise,notreturns 0.
在多数其他 shell(如 bash)中只支持 ! 前缀写法,而 fish 同时提供 not 与 ! 两种形式,后者只是前者的别名(alias),语义完全一致。与 bash 的一个关键差异是:fish 的 not/! 不需要像 bash 那样依赖 set -o pipefail 之类的选项来控制取反粒度——它对整个作业(job)的最终状态取反,这会在下文「实现原理」一节中详细展开。
语法格式(Synopsis)
not 的完整语法为:
not COMMAND [OPTIONS ...]
! COMMAND [OPTIONS ...]
即 not 或 ! 后紧跟一条命令,命令可以带自己的参数。两条语法形式完全等价:
not grep -q needle haystack.txt
! grep -q needle haystack.txt
两点注意:
not后跟的COMMAND可以是外部命令、内建命令(如test、string、math)、函数,甚至可以是另一个复合结构(如not begin ... end、not { ... }),它取反的是这条命令执行完毕后的最终退出状态。- 从测试用例 not.fish 中可以看到,
! -h在未重载的情况下会输出!的文档(Documentation for !),说明-h/--help选项由not自身处理,用于显示帮助信息,并不会被当作后面命令的参数。
核心行为:退出状态的取反规则
取反规则非常朴素,可以用下表概括:
| 原命令退出状态 | not 之后的退出状态 |
语义 |
|---|---|---|
| 0(成功) | 1 | 取反为失败 |
| 非 0(失败,如 1、2、34) | 0 | 取反为成功 |
该规则由源码直接印证。在 proc.rs 中,Job::statuses() 对作业最终状态的合成逻辑是:
st.status = if self.flags().negate {
if laststatus == 0 { 1 } else { 0 }
} else {
laststatus
};
也就是说,当作业带有取反标志(negate)时,只要最后一个进程的退出状态为 0,就输出 1;否则输出 0。这与文档描述逐字对应。
连续取反会互相抵消
not 可以连续堆叠,偶数次取反会互相抵消。这一点在 not.fish 测试中有明确验证:
# 一次取反:true 成功(0) -> 1
not true
echo $status
# CHECK: 1
# 两次取反:sh -c 'exit 34' 返回 34,取反两次后仍是 34
not not sh -c 'exit 34'
echo $status
# CHECK: 34
甚至可以在单行内混合使用 not 与 !。测试 andandoror.fish 中的用例:
not not not true # 三次取反 = 一次取反,最终 1
not ! ! not true # 四个前缀同样等价于一次取反
not ! echo not ! # echo 成功(0),取反后为 1
not 后的作业同时包含管道与多个进程
not 取反的是整个作业的最终状态,而不是管道中每一个进程的状态。例如:
not grep foo file.txt | head -n 1
这条命令中 not 作用于整个 grep ... | head ... 管道:管道最终状态由最后一个进程 head 决定,not 再对这个最终状态取反。其原理同样在 proc.rs 的 Job::statuses() 中体现——取反判断的是整个作业 laststatus(最后一个进程的退出状态),而 st.pipestatus 数组中仍然保留每个进程各自的状态,不会因 not 而改变。
实战场景:与 if / and / or 的组合用法
not 最常见的用途是作为条件判断的取反,出现在 if、and、or、while 等控制结构中。
在 if 条件中取反
文档 not.rst 给出了标准示例——当找不到名为 spoon 的文件时报错并退出:
if not test -f spoon
echo There is no spoon
exit 1
end
这里 test -f spoon 在文件存在时返回 0,not 将其取反为 1,于是 if 分支(echo There is no spoon; exit 1)仅在文件不存在时执行。这是「有则跳过、无则处理」类逻辑的惯用写法,等价于其他 shell 中的 if [ ! -f spoon ]; then ...; fi。
仓库内部也大量使用这一模式。例如 random.fish 中校验两个随机数相等的反例:
if not test "$run1" = "$run2"
# 两个数不同,继续逻辑
end
以及 symlinks-not-overwritten.fish 中对 $fish_variables 是否为符号链接的检查:
if not test -L $fish_variables
...
end
与 and / or 组合构造复合条件
not 可以自由地与 and、or 组合,构成复合判断。测试 andandoror.fish 中的用例:
or not false
这条命令中 false 失败(状态 1),not false 成功(状态 0),整条 or 连接的作业因此成功。这类写法可用于「当某个条件不成立时才执行后续逻辑」的链式表达。
在 while 循环条件中取反
同理,not 也可以放在 while 条件里,实现「当某命令失败时继续循环」的语义:
while not command -q python3
# 等待 python3 出现或进行其他处理
end
别名 ! 与重载注意事项
! 是 not 的别名,二者在语法上完全等价:
! test -f spoon
需要注意:! 在 fish 中并不是历史展开符(bash 中 ! 用于历史扩展),因此可以放心在脚本中使用,不会有 bash 用户常遇到的引号转义问题。
另一个值得注意的细节来自测试 not.fish:! 是可以被函数重载的。测试中演示了:
function !
echo overridden! $argv
end
! -h
# CHECK: overridden! -h
也就是说,如果你自定义了一个名为 ! 的函数,它会遮蔽内置的取反别名——这一点在 parser_keywords.rs 的保留字表中也有体现:! 被标记为 not reserved(不保留、允许重载),而 not 本身则是保留字(("not", [subcommand])),parser_keywords_is_reserved 返回 true,因此无法用同名函数或别名覆盖 not。
与其他关键字、命令的组合行为
not 作为保留关键字,还可以出现在一些更复杂的组合中,仓库测试提供了多个可验证的场景:
- 与
time组合(time.fish):
not time true # time 显示执行耗时,true 成功(0),取反后为 1
not time a=b true # 变量赋值 + time + 取反
- 与
exec组合(exec.fish):
not exec cat <nosuchfile
当 exec 因重定向失败而无法真正替换进程时,exec.rs 会按取反标志生成最终状态:let status = if job.flags().negate { 0 } else { 1 };——也就是说,exec 失败时 not exec ... 反而返回 0。
- 与变量赋值组合(variable-assignment.fish):
not a=b echo $a
- 与花括号/代码块组合(braces.fish):
not { false; true } # 块内最后一条 true 成功,取反后为 1
-
与 math 的错误输入配合(math.fish):
not math '2 - '、not math 'sqrt(-1)'等用例利用math对非法表达式返回非 0,再经not取反为 0,从而在测试中表达「该表达式应当报错」。 -
与
--分隔符组合(decorator-double-dash.fish):
not time -- { false }
实现原理:从语法解析到退出状态合成
not 不是普通的可执行文件,而是被 fish 解析器内建处理的关键字。它在源码中贯穿三条主线:
1. 词法/关键字识别阶段
在 parser_keywords.rs 的保留字表 RESERVED_WORDS 中,not 与 ! 均被登记,其中 not 是 [subcommand] 保留字(保留、不可重载),! 则是 [subcommand] 但 not reserved(不保留、可重载)。
在词法层面,parse_constants.rs 把字符串 "not" 映射为 ParseKeyword::Not:
'n' if s == L!("not") => ParseKeyword::Not,
而 ! 对应的关键字枚举在 parse_constants.rs 中定义为 Exclam(同文件 L238 中 ParseKeyword::Not => L!("not") 表明其显示名称仍为 not)。
2. 语法树(AST)阶段
在 ast.rs 中,当语句开头关键字为 Not 或 Exclam 时,解析器构造一个专门的 NotStatement 节点:
match self.peek_token(0).keyword {
ParseKeyword::Not | ParseKeyword::Exclam => {
let embedded = self.allocate_boxed_visit::<NotStatement>();
Statement::Not(embedded)
}
...
}
NotStatement 内部嵌有真实的命令内容(contents)以及可选的变量赋值列表(variables),这解释了上文 not a=b echo $a 能够正常工作的原因——变量赋值是 NotStatement 结构的一部分,会先于命令执行。
3. 执行与状态合成阶段
执行时,parse_execution.rs 的 populate_not_process 函数负责把取反意图记录到作业标志上:
fn populate_not_process(
&mut self,
ctx: &mut OperationContext<'_>,
job: &mut Job,
proc: &mut Process,
not_statement: &ast::NotStatement,
) -> EndExecutionReason {
{
let mut flags = job.flags_mut();
flags.negate = !flags.negate;
}
self.populate_job_process(
ctx, job, proc,
¬_statement.contents,
¬_statement.variables,
)
}
注意这里用的是 flags.negate = !flags.negate——即翻转标志而非简单置位。这正是 not not cmd 两次取反互相抵消、not not not true 三次取反等效一次的根本原因:每层 not 都会把 JobFlags::negate 翻转一次。
JobFlags::negate 定义于 proc.rs。最终状态合成就发生在 proc.rs 的 Job::statuses():取反标志为真时,最后一个进程状态为 0 则输出 1,否则输出 0(详见上文「核心行为」一节)。此外 exec.rs 对 exec 失败但带取反标志的作业也有同样的处理分支。
语法高亮
highlight.rs 将 ParseKeyword::Not 与 and、or 等一起作为「命令修饰关键字」着色,因此交互式输入时 not/! 会以关键字颜色显示,方便识别。
小结
not COMMAND与! COMMAND完全等价,作用是把整个作业的退出状态取反:0 → 1,非 0 → 0。- 连续多个
not/!会逐个翻转取反标志,偶数次相互抵消(源码依据:parse_execution.rs、not.fish)。 - 最典型的使用场景是
if not test -f file ...这类条件取反,也可与and/or/while、time、exec、变量赋值、代码块自由组合(仓库测试均有覆盖)。 not是保留关键字,不可重载;!不保留,可被同名函数覆盖(parser_keywords.rs)。- 从源码看,
not的实现横跨关键字表(parser_keywords.rs)、词法映射(parse_constants.rs)、AST 节点(ast.rs)、作业标志翻转(parse_execution.rs)与最终状态合成(proc.rs)五个环节,最终统一落地为JobFlags::negate这一单一标志。
若需深入验证,可阅读仓库中的官方手册 not.rst 与回归测试 not.fish、andandoror.fish,后者覆盖了本文提及的绝大多数取反组合场景。