- CLI
- 开发工具
【免费下载链接】fish-shell
The user-friendly command line shell.
导读
for是 fish shell 中最常用的循环结构,用于对一组值反复执行同一段命令。本文以官方文档 doc_src/cmds/for.rst 为骨架,完整讲解for的语法、空值列表行为、循环变量的作用域规则,并结合仓库源码与测试用例,深入剖析其底层执行流程、break/continue的配合方式,以及与 3.0.0 版本之前行为差异的来龙去脉。读完本文,你将能够熟练编写 fish 循环脚本,并理解循环变量在函数、全局、通用作用域之间的精确归属规则。
for 循环的基本语法
for是一个循环结构(loop construct),它会将COMMANDS指定的命令执行多次。每次迭代时,由VARNAME指定的局部变量会被赋予VALUES中的一个新值。官方语法如下:
for VARNAME in [VALUES ...]; COMMANDS ...; end其中:
VARNAME是循环变量名,必须是合法的 fish 变量名(不能是只读变量,详见下文源码分析);VALUES ...是迭代值列表,支持通配符、命令替换、变量展开,因此既可以是字面量,也可以是(seq 1 10)之类的动态结果;COMMANDS ...是循环体,可以包含任意 fish 命令,也可以嵌套其他循环、条件或函数调用;end是循环块的结束关键字,不可省略,否则会报"未配对的块"错误。
典型示例(来自文档 Example 一节):
for i in foo bar baz; echo $i; end输出结果为:
foo bar baz语法细节:
- 同一行的
for i in 1 2之后可以紧跟注释(# Comment on same line),注释不会影响循环解析,见 tests/checks/basic.fish 中的测试用例; - 迭代值列表可以跨行,通过行尾的反斜杠续行,例如
for i in \换行后继续写a b c,测试见 tests/checks/basic.fish; for与while一样都是块结构关键字,for、end等被列入保留字(reserved words),不允许被重载为函数名,相关定义见 src/parser_keywords.rs。
空值列表:循环体一次都不会执行
文档明确强调:如果VALUES为空,COMMANDS将完全不会执行("IfVALUESis empty,COMMANDSwill not be executed at all.")。例如:
for x in echo "never printed" end上述循环体不会输出任何内容。这一行为在 tests/checks/for.fish 中有直接验证:测试先把全局变量j设为global,再执行空的for j in循环,随后set -ql j && echo $j输出global,证明空列表下循环体未运行、变量未被改写。
循环变量与作用域规则
循环结束后变量仍然可见
文档指出:VARNAME在循环终止后仍然可见(visible when the loop terminates),并保存最后一次被赋予的值;如果循环开始前VARNAME不存在,它会被设置在局部作用域(local scope)中。
这是 fish 3.0.0 之后的关键变化。在此之前,for的循环变量只在循环块内部可见,循环结束后外部无法访问。文档给出了旧版本行为示例:
for var in a b c if break_from_loop break end end echo $var在 3.0.0 之前的版本中,echo $var输出的内容取决于循环运行前var被设置成了什么——通常是空("Likely nothing")。而在当前版本中,循环结束后$var一定等于最后一次迭代赋予的值(上例中为c)。
作用域的精确归属规则
文档进一步说明了循环变量的作用域查找规则,这是理解 fish 变量模型的关键:
- 如果
for块位于函数内部,那么循环体内必须存在一个同名的局部变量(即循环变量以局部变量的方式与函数共享作用域); - 如果
for块不在函数内(即顶层直接书写),那么已存在的同名全局变量或通用变量(universal)会被直接使用; - 如果同名变量在多个作用域都存在,按 fish 一贯的"最窄作用域优先"规则,更新的是最内层那个。
这些规则在 tests/checks/for.fish 中有非常清晰的用例:
# 循环变量是封闭作用域中的局部变量 set -g i global # 隐式执行 set -l i $i for i in local end set -ql i && echo $i # CHECK: local即:全局变量i存在,但for循环会创建局部的i并初始化为全局i的旧值,循环结束后局部i仍保留local。测试文件头部注释还特别说明:"A for-loop-variable is a local variable in the enclosing scope"(for 循环变量是封闭作用域中的一个局部变量)。
再来看另一个用例:
set -g j global for j in end set -ql j && echo $j # CHECK: global空的迭代列表不执行循环体,局部j在创建时被初始化为全局j的当前值,因此$j输出global。
嵌套循环的隔离性也有验证:
set -g k global begin for k in local1 echo $k # CHECK: local1 for k in local2 end echo $k # CHECK: local1(内层循环结束后,外层变量未被内层覆盖) end echo $k # CHECK: local1 end echo $k # CHECK: global(离开 begin 块后恢复全局值)这个用例同时验证了两点:内层for k in local2修改的是内层循环自己创建或沿用的局部变量,不会破坏外层循环的k;而begin...end块结束后,外层局部变量随之消失,$k重新取回全局值。
与 set 命令的对照
文档提到"Much likeset,fordoes not modify $status"——即for本身不改变$status,但循环体内命令的执行可以改变它("the evaluation of its subordinate commands can")。这与 set 命令文档 中"赋值模式不修改退出状态,只是透传命令替换设置的 status"是同一设计理念:控制结构本身不应吞掉或污染状态码,让脚本作者可以依赖循环体内最后一条命令的退出状态。
与 break / continue 的配合
for循环通常与break(提前终止循环)和continue(跳过本次迭代剩余部分)配合使用。continue 文档 中的示例展示了典型用法——跳过所有包含smurf的临时文件:
for i in *.tmp if grep smurf $i continue end # 如果执行了 continue,下面的 rm 会被跳过 rm $i end从底层实现看,break和continue内置命令只做一件事:把解析器libdata中的loop_status标记为LoopStatus::Breaks或LoopStatus::Continues(见 src/builtins/shared/misc.rs)。随后for的执行循环在每轮迭代结束后检查该标记,从而决定是跳出还是进入下一轮(详见下文实现原理)。如果break/continue出现在循环之外,会直接报错 "Not inside of loop"。
源码级实现原理
for语句的实际执行位于 src/parse_execution.rs 的run_for_statement函数中,其执行流程与文档描述一一对应:
- 解析变量名:从 AST 中取出
for var_name in ...的变量名,先做一次展开(expand_one),再校验其是否为合法变量名(valid_var_name)。如果变量名本身展开失败或非法,报错并返回STATUS_EXPAND_ERROR或STATUS_INVALID_ARGS。 - 校验只读变量:通过
EnvVar::flags_for检查变量是否为只读(例如PWD这类特殊只读变量)。若为只读,报错 "for: ...: cannot overwrite read-only variable"。这一点与 tests/checks/basic.fish 中for PWD in foo bar报错的测试用例吻合。 - 初始化循环变量:在进入循环前,先用
set_var(..., EnvMode::LOCAL, 旧值)将循环变量按局部作用域设置,旧值来自当前已存在的同名变量(这正是"如果 VARNAME 已存在,则继承其旧值"这一行为的来源)。 - 展开迭代值列表:将
VALUES ...整体展开为参数列表。注意通配符使用WildcardNoMatchBehavior::Allow,即匹配不到任何文件时不报错、得到空列表,从而保证"空列表不执行循环体"。 - 驱动循环:逐值把循环变量设置为当前迭代值(
EnvMode::default(),即沿用既有作用域规则),触发一次变量变更事件(Event::variable_set),然后push_block(Block::for_block())执行循环体中的 job list,结束后pop_block清理块内变量。 - 处理 break/continue:每轮迭代检查
loop_status,若为LoopStatus::Breaks则跳出循环;若为LoopStatus::Continues则进入下一轮。循环开始时会把loop_status重置为Normals。
此外,for作为保留字,其解析与while、begin、function等块结构共用同一套块语句分发逻辑(src/parse_execution.rs),这也是 fish 各种块结构在解析和报错行为上高度一致的原因。
变量变更事件
循环每次迭代都会触发Event::variable_set事件。这意味着你可以用function --on-variable foo之类的函数监听循环变量的每一次更新。这一行为同样有测试覆盖(tests/checks/for.fish):对for foo in 1 2 3循环,监听foo变量的函数被触发了三次(CHECK: foo set出现三行)。
实战示例
组合运用以上特性,可以写出既简洁又符合 fish 风格(默认变量即列表)的循环脚本。
遍历文件并对每个文件执行操作:
for f in *.log echo "processing $f" end配合命令替换动态生成迭代列表:
for i in (seq 1 5) echo "item $i" end提前跳出循环:
for i in (seq 1 10) if test $i -eq 3 break end echo $i end # 输出 1 2循环结束后继续使用最后一个值:
for server in db web cache echo "starting $server" end echo "last was $server" # 输出 last was cache嵌套循环打印二维组合:
for i in 1 2 for j in a b echo $i$j end end # 输出 1a 1b 2a 2b最后一个例子直接来自 tests/checks/basic.fish 的测试用例,验证了多行注释、循环体内注释以及嵌套循环均可正常工作。
总结与版本注意事项
for VARNAME in VALUES...; COMMANDS; end是 fish 的循环核心语法,VALUES为空则循环体完全不执行;- 3.0.0 起循环变量在循环结束后仍然可见,并保留最后赋给它的值;3.0.0 之前循环变量仅限循环块内部,外部
echo $var通常为空——升级旧脚本时请留意这一行为差异; - 循环变量默认落在局部作用域:函数内与函数局部作用域共享,顶层则沿用已存在的全局/通用变量,找不到时按 fish 规则新建局部变量;
for本身不修改$status,循环体内命令的退出状态可以自由透传;- 底层实现可查阅 src/parse_execution.rs,行为测试可查阅 tests/checks/for.fish 与 tests/checks/basic.fish,相关命令文档见 for、while、break、continue、set。
- CLI
- 开发工具
【免费下载链接】fish-shell
The user-friendly command line shell.
相关推荐
告别环境变量混乱:Fish Shell作用域与导出机制全解析
告别环境变量混乱:Fish Shell作用域与导出机制全解析 Fish Shell 作为一款用户友好的命令行 shell,提供了清晰的环境变量管理机制,帮助用户
CLI开发工具Fish Shell 3.7.1 中局部变量作用域问题的技术分析
Fish Shell 3.7.1 中局部变量作用域问题的技术分析 在 Fish Shell 3.7.1 版本中发现了一个关于局部变量作用域的有趣问题,这个问题涉
CLI开发工具Roc 语言嵌套 for 循环实战:基于 REPL 快照测试解析迭代、变量重赋值与作用域
Roc 语言嵌套 for 循环实战:基于 REPL 快照测试解析迭代、变量重赋值与作用域 本指南以 Roc 仓库中 REPL 快照测试 for_loop_nes
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考