关于shell 作业控制的标题卡,配有箭头连接的通用 Observe、Decide、Act 面板。

作业控制在进程组之间交接终端前台

交互式作业控制让 shell 管理共享控制终端的多个进程组。终端同一时刻只有一个前台组,其他作业可以在后台运行,也可以保持停止状态。前台归属、是否运行和 shell 的作业表,是三种不同状态。后台作业不等于已经暂停,停止作业也不等于已经终止;判断当前交互为什么没有进展,需要先分清这些状态。 坏掉的样子 启用终端信号处理时,输入已配置的中断或暂停字符,会由终端驱动向前台组发送信号,不是只发送给最后打印文字的那个进程。控制字符重新映射、终端模式变化以及应用信号处理函数,都可能改变结果。因此只说按某个键,并不能保证在每个程序和每套终端配置中得到相同反应,还要核对实际处理条件。 普通后台作业尝试读取自己的控制终端时,会被 SIGTTIN 停止。把它放到后台,并不会给它单独分配一份键盘输入流。输出又有不同规则:后台写入通常允许,还会受 TOSTOP 等终端设置影响。因此后台执行不意味着彻底脱离终端,输入、输出和前台信号各有自己的约束,需要分别观察。 %1 这类作业标识属于某个 shell 的作业表,不是全局进程编号。换一个终端或进入非交互 shell 后,不能假定相同号码仍然管理相同任务。管道和后台命令如何分组,也取决于真实 shell 调用是否启用了作业控制。交互终端中看见的行为,不能不加检查地套到所有脚本启动路径上。 快速验证 把脚本保存为 jobcontrol.py,在装有 Bash 和 Python 的 Linux 环境中运行 python3 jobcontrol.py。本轮使用 Linux 6.18.44、Bash 5.2.37 和 Python 3.12.14 原样重跑了两种语言的样本。脚本创建自己的伪终端与交互 Bash,不会把控制字符发给你用于启动它的终端: from pathlib import Path import os,pty,re,select,signal,termios,time pid,master=pty.fork() if pid==0: attributes=termios.tcgetattr(0);attributes[3]&=~termios.ECHO;attributes[3]|=termios.ISIG|termios.ICANON attributes[6][termios.VINTR]=b'\x03';attributes[6][termios.VSUSP]=b'\x1a';attributes[6][termios.VEOF]=b'\x04' termios.tcsetattr(0,termios.TCSANOW,attributes) os.execve('/bin/bash',['bash','--noprofile','--norc','-i'],{'PATH':'/usr/bin:/bin','TERM':'dumb','LC_ALL':'C','PS1':'XB-PROMPT> ','HISTFILE':'/dev/null'}) known_groups={pid};pending=b'' def until_prompt(): global pending deadline=time.monotonic()+5 while b'XB-PROMPT> ' not in pending: remaining=deadline-time.monotonic();assert remaining>0,pending ready,_,_=select.select([master],[],[],remaining);assert ready,pending chunk=os.read(master,65536);assert chunk,'PTY closed before prompt';pending+=chunk output,pending=pending.split(b'XB-PROMPT> ',1) return output.decode(errors='replace') def command(text):os.write(master,text.encode()+b'\n');return until_prompt() def wait_for(predicate): deadline=time.monotonic()+5 while not predicate(): assert time.monotonic()<deadline,'terminal state did not change';time.sleep(0.01) def state(process): try:return (Path('/proc')/str(process)/'stat').read_text().rsplit(') ',1)[1].split()[0] except FileNotFoundError:return None try: until_prompt();assert os.tcgetpgrp(master)==pid os.write(master,b'sleep 60 | cat\n') wait_for(lambda:os.tcgetpgrp(master)!=pid) group=os.tcgetpgrp(master);known_groups.add(group) def pipeline_started(): children=[] for process in Path('/proc').iterdir(): if not process.name.isdecimal():continue try: fields=(process/'stat').read_text().rsplit(') ',1)[1].split() if int(fields[1])==pid: children.append(((process/'comm').read_text().strip(),int(fields[2]))) except (FileNotFoundError,ProcessLookupError):continue return sorted(children)==[('cat',group),('sleep',group)] wait_for(pipeline_started) os.write(master,b'\x1a');output=until_prompt() assert 'Stopped' in output and os.tcgetpgrp(master)==pid,output assert 'Stopped' in command('jobs -l') print('Ctrl-Z stops the foreground pipeline and returns the terminal to the interactive shell') command('bg %1');assert os.tcgetpgrp(master)==pid os.write(master,b'fg %1\n');wait_for(lambda:os.tcgetpgrp(master)==group) os.write(master,b'\x03');until_prompt() assert 'RESULT:130' in command("printf 'RESULT:%s\\n' \"$?\"") print('bg resumes without foreground ownership; fg restores ownership; Ctrl-C gives this pipeline status 130') output=command("cat & printf 'BG:%s\\n' \"$!\"") background=int(re.search(r'BG:(\d+)',output)[1]);known_groups.add(background) wait_for(lambda:state(background)=='T') assert os.tcgetpgrp(master)==pid assert 'Stopped (tty input)' in command('jobs -l') print('a background reader is stopped by terminal input rules, rather than receiving the shell keyboard stream') os.write(master,b'fg %+\n');wait_for(lambda:os.tcgetpgrp(master)==background) os.write(master,b'hello\n\x04');output=until_prompt();assert 'hello' in output,output assert 'RESULT:0' in command("printf 'RESULT:%s\\n' \"$?\"") print('foregrounding that reader lets it read; EOF ends it normally; all jobs are reaped') assert not command('jobs -p').strip() os.write(master,b'exit\n');waited,status=os.waitpid(pid,0);assert waited==pid and os.WIFEXITED(status) pid=None finally: if pid is not None: for group in known_groups: try:os.killpg(group,signal.SIGKILL) except ProcessLookupError:pass os.waitpid(pid,0) os.close(master) 样本启用终端信号处理和规范输入模式,明确配置中断、暂停与 EOF 字符,并关闭输入回显。Bash 启动时不加载用户配置文件,使用可识别提示符。这些条件使实验不依赖用户别名、提示符命令或自定义快捷键;观察到的行为来自样本明确建立的终端状态,而不是碰巧继承的交互环境。 ...

九月 27, 2027 · 2 分钟 · XB Field Notes
标题为「环境变量属于各自的进程」的封面,下方是观察、判断与行动三个面板。

环境变量属于各自的进程

环境是程序启动时收到的一组名称与字符串值。子进程通常接收父进程选定的环境,修改自己的副本不会回写父进程或兄弟进程。这是一条有用的配置边界,却不是命令没有副作用的保证:程序仍可能根据配置修改共享文件、数据库或其他资源。理解变量如何传递,与判断程序实际会做什么,是两个需要分别核对的问题。 导出决定另一个程序能收到什么 Shell 变量与环境条目有关联,但并不是同一概念。DEMO_SETTING=local 创建或更新 Shell 参数,export DEMO_SETTING 才把它标记为启动外部命令时应包含的环境项。父 Shell 能打印变量,不代表新启动的 Bash 就能看到未导出的值。把赋值写进某个配置文件,也不会让每个 Shell 自动读取它;还必须确认文件是否在对应启动路径上被加载。 Bash 从环境继承的变量已经带有导出属性,之后重新赋值会保留该属性。因此子进程修改值后,再启动孙进程时可以直接传递新值,不要求每一代重复写一次导出命令。unset 删除变量,Bash 的 export -n 则取消导出属性但保留当前 Shell 中的值。真正需要确认的是直接启动者传入了什么,而不是机械地搜索祖先脚本里出现了多少次导出语句。 子 Shell 不等于重新执行一个 Shell 程序 圆括号命令组和命令替换使用从当前 Shell 复制出来的子 Shell 环境,所以能够看到从未导出的普通 Shell 变量。它们的修改仍不会回到外层。这与重新执行一个 Bash 程序不同:后者收到的是导出的环境条目,不是父 Shell 整张参数表。若把两种情况都简单称为“子进程只认识已导出变量”,就会错误解释圆括号内的实际行为。 使用点命令加载文件时,文件中的命令在当前 Shell 执行,赋值因而能直接改变调用者。这并没有打破进程边界,因为根本不是另一个进程把修改传回来。被加载的文件是可执行 Shell 代码,不是被动的数据文件;只有当你确实打算在当前上下文执行那些内容时,才应加载它。文件扩展名叫配置,并不会改变这一点。 ...

九月 25, 2027 · 1 分钟 · XB Field Notes
观察、决策、行动三个面板,旁边是关于Bash 文件名模式展开的标题。

Bash 的通配符在命令执行前就展开

Bash 命令中的通配符通常会在目标程序启动前变成一组路径。它让批量处理很方便,也让同一行命令受到当前目录和 shell 选项的影响。程序拿到的是已经形成的参数,不会收到标记来告诉它哪些参数来自星号。因此,写命令时应先确定:希望传入实际匹配的文件,还是希望把模式本身交给另一个程序解释。两者需要不同的引号写法,不能依靠目标程序猜测意图。 先匹配,再保留参数边界 Bash 先进行分词,再进行文件名展开。没有引用的星号、问号和方括号模式可能因此生成多个参数。但匹配到的文件名即使包含空格,也仍然是一个参数,因为文件名展开之后不会再执行一次分词。常见问题发生在下一步:循环拿到了完整路径,却用没有引号的变量把它传给另一个命令。循环体中的 "$file" 仍然必要,不能因为前一步通配成功就省掉。 如果接收方需要模式,就应引用模式。例如 find . -name '*.txt' 将匹配规则交给 find;去掉引号后,Bash 会先在当前目录展开它,find 得到的可能是多个文件名,已经不是原来要表达的条件。变量里包含星号时也一样:引用变量会传递字面星号,不引用则可能触发文件名展开。引号只控制当前 shell 如何形成参数,不会禁止接收方按自己的语法解释收到的文本。 通配符匹配目录项,并不保证结果都是普通文件。名为 archive.txt 的目录也能匹配 *.txt,只想处理普通文件时还需要判断类型。普通星号不会越过斜杠,默认也不匹配名字开头的点;需要隐藏文件时,应明确使用相应模式或开启 dotglob。遍历整棵目录树是另一个需求,不能把当前目录的一次展开误认为递归搜索。 环境变量也能改变结果集合。Bash 中非空的 GLOBIGNORE 会过滤匹配结果,并且产生开启 dotglob 的副作用。因此,从交互会话继承状态的脚本可能意外纳入隐藏文件。下面的实验会清除 GLOBIGNORE 及较新版本的排序覆盖变量 GLOBSORT,并明确指定相关选项,让断言对应一套可重复的环境,而不是依赖读者终端的偶然配置。 坏掉的样子 默认情况下,没有匹配结果的模式会保留为原文。因此,遍历 *.png 的循环可能仍然运行一次,变量内容就是一个并不存在的星号文件名。开启 nullglob 后,未匹配的模式会直接消失,但这也可能让接收方变成零文件参数调用。某些程序此时等待标准输入,另一些程序则使用默认目标。未定义变量检查和管道失败检查都不能证明通配符至少找到了一个文件。 对于允许为空的输入,可以在开启 nullglob 后先收集到 Bash 数组,检查元素数量,再决定是否调用消费者。对于必须存在的输入,failglob 会在未匹配时报告展开错误,并阻止该命令执行。如果只想临时改变行为,把选项修改放在子 shell 中,避免影响调用者的后续命令。交互终端的启动文件可能已经修改选项,所以终端上一次成功不能替代脚本自己的前置条件。 ...

九月 8, 2027 · 1 分钟 · XB Field Notes
以观察、判断和行动三个面板呈现命令查找诊断。

shell 找不到那条裸命令时

“找不到命令”说明一次查找失败,却不代表 Shell 每次都遍历了 PATH 的全部目录。在普通 Bash 中,不带斜杠的命令词可能对应函数、内建命令、缓存的可执行路径,或者刚刚搜索到的程序。更早的输入读取阶段,别名还可能已经改写命令。面对报错,应先检查真正发生故障的那个 Shell,而不是立即重装一个其实已经存在的工具。 先确认是哪一层查找给出结果 展开之后,Bash 先检查函数,再检查内建命令,之后才查找外部程序。type name 描述当前解析结果,type -a name 可以看到其他定义或候选项。command -v name 适合检查该 Shell 中是否存在某个命令,但结果不保证是文件路径,也可能说明它是函数或内建命令。因此不能无条件把输出交给复制文件或读取文件的工具。 查找外部程序时,PATH 中的目录按顺序参与搜索,靠前的旧版本可以遮住后面的新版本。Bash 还会缓存成功找到的路径:hash -t name 可以检查缓存项,hash -r 清空缓存表。原路径上的文件发生变化,与缓存仍选择旧位置,是两个不同的问题。另开一个终端可能重置这些状态,让故障暂时消失,却没有解释原会话到底发生了什么。 斜杠直接选择路径 命令词中的任何斜杠都会绕过外部程序的 PATH 搜索。./tool 指向当前目录的文件,/opt/example/bin/tool 则指定绝对位置。单独输入 tool 并不自动包含当前目录;只有空目录项、相对目录项等配置才可能让当前工作目录介入搜索。这意味着相同的命令名,在不同目录下也可能选中不同的可执行文件。 给服务配置环境时,应明确列出可信目录。交互终端能运行,不证明定时任务、服务管理器或另一个用户拥有相同的环境和启动文件。排查时需要比较实际环境与工作目录。修改登录配置文件也不会自动更新已经运行的进程;应该把预期环境应用到真正负责启动命令的进程,再验证原来的调用链。 坏掉的样子 Bash 通常在找不到命令时返回 127,在找到却无法执行时返回 126。这两个值可以引导调查,却不能取代诊断文本:脚本文件明明存在,但首行指定的解释器不存在,同样可能执行失败。应同时检查选中的文件、执行权限与解释器,而不是反复向搜索路径追加目录,最后把问题掩盖成一次偶然成功。 ...

五月 20, 2027 · 1 分钟 · XB Field Notes
脚本可见、能直接把整份位置参数暴露出来的内建变量。

$@ 等特殊变量给出函数的完整入参清单

包装脚本应当把收到的参数按原有边界继续传递。在 Bash 的命令参数位置,"$@" 会让每个位置参数分别成为一个带引号的词,因此能保留空值、空格、换行和字面通配符。只用两个简单文件名测试成功,并不能证明包装脚本正确;一旦它把参数列表拼成字符串再拆开,真实调用者传入的边界就可能丢失。转发是否可靠,应当用能暴露这种差异的输入来验证。 参数列表与拼接文本不同 位置参数属于当前脚本或正在执行的函数。$# 返回参数数量,内容为空字符串的参数也计算在内。"$1" 读取第一个参数,"${10}" 读取第十个;后者需要花括号,以区别于第一个参数后面接字符零的写法。$0 表示启动的 shell 或脚本名称,不会作为额外元素出现在 "$@" 中,不能把它误算进业务参数列表。 在普通命令边界上,"$@" 为每个参数保留一个独立词;完全没有位置参数时,它不产生任何参数。相比之下,"$*" 始终形成一个字符串,用 IFS 的第一个字符连接各个值。IFS 未设置时使用空格,设为空字符串时则不插入分隔符。它并不会删除参数内部原有的空格,但各参数原来的边界通常已经无法恢复,因此不适合转发任意列表。 省略引号的 $@ 不是等价简写,展开后的内容仍可能被分词和文件名展开处理。调用者传入的字面星号,到了包装脚本所在目录可能突然匹配出一组文件。往拼接文本里补引号字符也不能恢复结构。需要把列表保存到之后使用时,在 Bash 中写成 args=("$@"),并通过 "${args[@]}" 转发,即使随后调整位置参数,保存的列表仍保留各个元素。 坏掉的样子 函数执行期间拥有自己的位置参数列表。调用时传入的参数暂时替换当前可见列表,返回时恢复调用者的列表。函数内部使用 set 或 shift 修改的是这次函数调用的参数,但这不代表其他所有 shell 状态都自动局部化。例如,没有局部声明就修改 IFS,可能影响调用者之后的命令。参数作用域和普通变量作用域应分别理解,不能互相推导。 Shift 会删除前面的若干参数,并对剩余参数重新编号。解析选项时,必须先确认剩余参数足够,再消费选项及其值;要求删除的数量超过现有参数时会失败。仅检查当前数量大于零,还不足以处理需要额外值的选项。如果之后的错误提示或转发还需要被消费的原始值,应在开始解析前保存数组,避免试图从拼接日志中重新恢复。 引号也不能阻止接收程序把完整参数解释为选项。例如,文件名恰好为 --help 时,"$@" 会完整保留它,但目标工具仍可能显示帮助而不读取文件。传入操作数时,应使用工具文档规定的选项终止符,或适当添加路径前缀。包装脚本必须明确自己的选项在哪里结束,以及哪些剩余参数仍是下游命令的选项,不能不加区分地自动插入分隔符。 ...

四月 16, 2027 · 2 分钟 · XB Field Notes
以观察、判断和行动三个面板呈现命令状态解释。

退出码是用一个数字汇报成败

退出状态是 shell 用来决定下一步执行什么的小整数。对 shell 控制流来说,零表示成功,非零进入失败分支,但这并不意味着每个数字都有统一的业务含义。搜索没有匹配、输入格式错误和服务器不可用,都可能返回非零,却需要不同处理。决定重试、忽略或终止之前,应先阅读目标命令的契约。不能只因为状态不为零,就把所有结果归为同一种事故。 让状态始终对应原来的命令 Bash 通过 $? 提供最近前台命令的状态。需要保留时,必须在记录日志、执行另一个条件判断或清理操作之前保存;这些操作也有自己的状态,会覆盖原本想观察的证据。函数通常使用最后执行的命令状态作为返回值,因此,末尾一句成功的日志输出就可能把前面的失败包装成整个函数成功,调用者看到的结果与实际工作脱节。 当命令成功与否决定后续动作时,可以使用 if command; then ... else ... fi。如果需要精确退出码,在 else 分支第一步保存 $?,输出诊断或清理后再通过 return 或 exit 显式传播。不要写成 if ! command,却期待进入成功分支后的 $? 仍然是原始失败码:逻辑取反已经把非零结果转换成成功,数值含义在这里发生了变化。 Bash 在找不到命令时使用一百二十七,找到却无法执行时使用一百二十六。因致命信号终止时,它使用一百二十八加信号编号的约定。这些规则有助于理解 shell 的报告,但应用也可以主动返回相同数字。因此,只有一个状态值还不足以证明内部具体发生了哪件事;应同时保留失败操作的身份和标准错误信息,避免根据数字臆造故障原因。 坏掉的样子 管道默认使用最后一段的退出状态。上游生成器失败时,下游仍可能成功读取一个空流或不完整数据,从而让整体看起来成功。开启 pipefail 后,Bash 返回从左到右最靠右的非零状态;全部成功才为零。PIPESTATUS 能提供每一段的结果,但后续命令同样会覆盖这个数组,所以必须先完整保存观察结果,再进行打印或判断。 set -e 开启的 errexit 具有由语法和调用上下文决定的例外。作为 if 条件的失败、与或列表中的部分命令,以及其他文档规定的场景,都不会自动结束脚本。函数被用作条件时,函数体中的失败命令之后也可能继续执行。ERR trap 也有类似排除规则,并不是每次失败都会产生的通知机制。两者都不能证明整个脚本已经可靠处理所有错误。 ...

三月 27, 2027 · 1 分钟 · XB Field Notes
以观察、判断和行动三个面板呈现函数参数与共享状态。

Shell 函数像脚本一样读取自己的参数

Shell 函数把一组命令组织到同一个名称之下,并接收本次调用自己的位置参数。普通调用时,函数体在当前 Shell 中执行,不会自动启动独立进程。这方便复用操作,也意味着赋值、切换目录及其他 Shell 状态变化可能影响调用方。函数接口除了规定输入,还应明确输出、退出状态和有意产生的副作用,不能只因为把几行命令包进了函数,就认为它们已经与外层隔离。 每次调用都有自己的参数列表 调用 demo_func first second 后,这次函数内部的前两个位置分别是传入的两个值,参数数量为二。再次调用其他函数,会暂时安装下一层自己的参数列表;返回后,调用者的列表重新可见。因此,在被调用函数中移位,不会消耗调用者的位置参数。Bash 的 $0 也不会因此变成当前函数名称,不能用它判断自己正在哪个函数里面。 使用 "$@" 转发整份列表,才能保留空参数、空格以及通配符字符各自的边界。应先检查数量,再读取必需的位置,同时区分省略参数与明确传入空字符串。Shell 函数没有自动执行这些规则的参数声明,调用时语法上能写出一串值,并不意味着这些值已经满足函数的业务约定。 局部变量使用动态作用域 Bash 的局部声明会创建一个变量,遮住外层的同名绑定;函数返回之后,原来的绑定再次可见。若修改调用者变量并不是接口的一部分,应为临时值、循环计数器等声明局部变量。面向多个解释器的可移植脚本,不能假设每个 Shell 都实现了 Bash 的这套局部变量设施,解释器要求必须与使用的语法保持一致。 但 Bash 采用动态作用域:被另一个函数调用的函数,可以看到调用者已经声明的局部变量。如果它没有声明自己的局部变量,就直接给同名变量赋值,修改的可能正是调用者的局部绑定。最外层的全局值依然受保护,但中间那层函数的值已经改变。因此,局部变量不是其他函数完全不能接触的词法私有对象,沿真实调用链测试才看得见这种影响。 普通的局部变量声明不会自动恢复工作目录、Shell 选项、陷阱,或函数体已经改写的文件。Bash 另外提供 local -,可以在函数返回时恢复通过 set 改动的选项;这是一项明确的选项作用域机制,与单纯声明临时变量不同。这些效果都需要各自的约定。如果把函数放进普通命令替换,它会在子 Shell 环境中运行,变量和目录变化不会回到外层,但文件写入仍可能保留。捕获输出改变的是执行边界,不是为任意操作提供一套安全沙箱。 状态与数据走不同通道 没有显式返回时,函数通常采用最后执行命令的状态。标准输出应承载约定的数据,诊断则写到标准错误。return 7 把一个状态交给调用者,不是返回文本数字作为数据;exit 会结束正在执行的 Shell,而不只是离开当前函数。设计辅助函数时,需要让调用者知道究竟应读取哪个通道,以及失败后还能不能使用已经打印出的部分结果。 ...

三月 16, 2027 · 1 分钟 · XB Field Notes
以观察、判断和行动三个面板呈现文本捕获与退出状态。

命令替换把一条命令的输出嵌进另一行

命令替换会收集一个命令的标准输出,再把得到的文本放入外层表达式。常见的 $(command) 适合获取时间戳、计算出的标签或其他较小的文本值,但它并不是任意字节的透明传输通道,也不是可靠的文件名列表容器。使用前需要理解三个边界:末尾换行如何处理、内部命令在哪个执行环境运行,以及失败状态如何传递。只看到变量里有内容,不能证明整个操作成功。 先收集文本,再决定怎样使用 本文讨论的标准命令替换形式会让 Bash 在子 shell 环境中运行内部命令,收集标准输出,并删除末尾所有换行符。位于文本中间的换行仍然保留。双引号能阻止后续分词和文件名展开,却不能恢复已经删除的尾部换行。如果需要精确保留最后几个字节,应该使用文件或数据流,而不是依靠 shell 变量原样往返。 赋值语句 result=$(producer) 会把处理后的文本存进变量,不在此处拆成多个参数。后续使用时仍应写成 "$result",保留它的参数边界。类似地,consumer "$(producer)" 会传递一个参数,即使结果为空。去掉引号可能得到多个参数,也可能完全没有参数;输出中的星号还可能匹配当前目录的文件,让同样的生产者输出在不同目录产生不同调用结果。 标准错误和标准输出彼此独立,除非主动重定向,诊断信息仍可能显示在终端,而变量只收集结果正文。将两路输出合并后,警告文本可能污染本应供程序读取的值。因此,应先确定生产者的输出契约,再决定是否合并;直接吞掉所有诊断信息也可能让空结果失去可解释的原因。输出内容、诊断信息和退出状态应分别检查。 不要把这里的作用域结论推广到较新 Bash 的每一种捕获语法。本文使用的美元符号加圆括号形式具有实验验证的子 Shell 行为;较新的当前环境捕获形式会保留不同的副作用。说明一个捕获值时,应明确语法与解释器版本,而不是把所有能够取得文本的写法都视为等价。 坏掉的样子 子 shell 内修改变量或执行 cd,不会改变调用者对应的变量和工作目录。但文件系统或网络副作用并不会因此消失:在命令替换里创建的文件是真实文件,发出的请求也是真实请求。执行环境隔离不能被误解为事务回滚,也不能保证内部操作没有外部影响。 有输出也不代表生产者成功。仅包含赋值的命令会使用最后一个命令替换的退出状态,因此 if result=$(producer); then 可以检查生产者结果。相反,printf '%s' "$(producer)" 的状态通常来自 printf,而不是内部命令。生产者可以先输出部分内容,再失败退出;应立即保存退出码,并明确失败时是否允许使用这些不完整内容。后面的打印日志命令会覆盖 $? 所指向的最近状态。 多条命令组成的替换尤其需要明确失败传播:前一步失败后,最后一步仍可能成功,从而使整个替换返回成功。重要操作应在生产者内部显式判断,并返回预期状态。函数中如果需要检查捕获赋值的状态,也应把 local 声明和赋值分成两步,避免声明内建命令掩盖生产者退出码。不能仅凭脚本开启了某个全局选项,就假设每种上下文都会按同样方式中止。 ...

三月 3, 2027 · 1 分钟 · XB Field Notes