拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

fish shell 的 for 循环:语法、变量作用域与实现原理全解析

fish shell 的 for 循环:语法、变量作用域与实现原理全解析
  • CLI
  • 开发工具

【免费下载链接】fish-shell

The user-friendly command line shell.

项目地址:https://gitcode.com/GitHub_Trending/fi/fish-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函数中,其执行流程与文档描述一一对应:

  1. 解析变量名:从 AST 中取出for var_name in ...的变量名,先做一次展开(expand_one),再校验其是否为合法变量名(valid_var_name)。如果变量名本身展开失败或非法,报错并返回STATUS_EXPAND_ERROR或STATUS_INVALID_ARGS。
  2. 校验只读变量:通过EnvVar::flags_for检查变量是否为只读(例如PWD这类特殊只读变量)。若为只读,报错 "for: ...: cannot overwrite read-only variable"。这一点与 tests/checks/basic.fish 中for PWD in foo bar报错的测试用例吻合。
  3. 初始化循环变量:在进入循环前,先用set_var(..., EnvMode::LOCAL, 旧值)将循环变量按局部作用域设置,旧值来自当前已存在的同名变量(这正是"如果 VARNAME 已存在,则继承其旧值"这一行为的来源)。
  4. 展开迭代值列表:将VALUES ...整体展开为参数列表。注意通配符使用WildcardNoMatchBehavior::Allow,即匹配不到任何文件时不报错、得到空列表,从而保证"空列表不执行循环体"。
  5. 驱动循环:逐值把循环变量设置为当前迭代值(EnvMode::default(),即沿用既有作用域规则),触发一次变量变更事件(Event::variable_set),然后push_block(Block::for_block())执行循环体中的 job list,结束后pop_block清理块内变量。
  6. 处理 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.

项目地址:https://gitcode.com/GitHub_Trending/fi/fish-shell
点击查看免费下载
上一篇:Streama视频弹幕功能实现:WebSocket与前端渲染
下一篇:3步构建LLM评估可视化系统:从黑箱测试到透明分析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表