news 2026/9/17 2:56:53

Foundry 输出通道契约(Output Channels):stdout/stderr 分离规范与 sh_* 宏实践指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Foundry 输出通道契约(Output Channels):stdout/stderr 分离规范与 sh_* 宏实践指南

Foundry 输出通道契约(Output Channels):stdout/stderr 分离规范与 sh_* 宏实践指南

【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry

Foundry 作为一个由forgecastanvilchiselscriptverify等多个 CLI 工具组成的 Rust 工作区,其命令行输出能否被脚本可靠解析,直接决定了自动化流水线的可行性。本文档围绕 docs/dev/output-channels.md 定义的输出通道契约展开:它规定 stdout 只承载命令的机器可读主结果、stderr 承载一切诊断信息,并配套提供sh_*宏体系与 clippy 强制 lint 来保证实现不偏离契约。读完本文,你将掌握 Foundry 各命令 stdout 的规范格式(含 per-command 对照表)、sh_*宏的选型规则、--quiet/--json/-vvv与通道的交互语义,以及如何在编写代码时通过源码级机制守住这条契约。

契约总述:stdout 是结果,stderr 是其余一切

契约的核心只有两句话:

  • stdout是命令的主要结果(primary result),除此之外什么都不应该出现。文本模式下,它是一行规范化的值(多列输出时按制表符\t分隔);--json模式下,它是单个 JSON 文档
  • stderr承载命令发出的所有其他字节:警告、错误、进度指示、状态文案、提示语、横幅(banner)、ABI 转储、验证过程中的各种闲聊输出(verification chatter)。

这套契约的验收标准非常直白:任何 Agent 或 shell 脚本都可以运行任意命令、丢弃 stderr,并信任 stdout 只包含文档规定的机器可读结果。典型的用法示例:

forge create … 2>/dev/null # → 只得到合约地址 forge test --json 2>/dev/null | jq … # → 永远是合法 JSON cast call … 2>/dev/null | xargs … # → 只得到返回值

注意最后一条示例的用途:cast call的返回值被直接喂给xargs,如果 stdout 混入了任何状态文案,管道解析就会失败——这正是该契约存在的意义。

由契约推导出的推论(Corollaries)

契约原文给出了五条推论,它们界定了各输出选项与通道之间的边界:

  1. --json只改变 stdout 的格式,绝不改变通道的纯净度——它不会把诊断信息挪进 stdout,也不会让 stdout 变脏。
  2. --quiet抑制 stderr 诊断与进度输出。目标契约要求它永远不改变 stdout 内容;但当前实现中sh_print!/sh_println!仍会被--quiet抑制(见 macros.rs 中print_out的 TODO 注释),这一"旁路"(bypass)将在forge/script中主要的散文式 stdout 调用点迁移到sh_status!之后翻转。sh_err!是文档明确列出的例外:致命错误永远输出到 stderr,且不受--quiet抑制
  3. -vvv增加 stderr 的详细程度,但它绝不能改变 stdout 内容
  4. 没有主结果的命令(例如forge install)默认不向 stdout 写入任何内容
  5. 提示语(Prompt)属于诊断信息:问题写到 stderr,答案从 stdin 读取。

如何编写符合契约的代码:sh_* 宏体系

要写符合契约的代码,规则只有一条:使用foundry_common::io提供的sh_*宏,不要直接调用println!/eprintln!

为了从工程上强制这一点,仓库在根目录 clippy.toml 中配置了工作区级的disallowed-macroslint,直接禁用四个标准库宏:

disallowed-macros = [ # See `foundry_common::shell`. { path = "std::print", reason = "use `sh_print` or similar macros instead" }, { path = "std::eprint", reason = "use `sh_eprint` or similar macros instead" }, { path = "std::println", reason = "use `sh_println` or similar macros instead" }, { path = "std::eprintln", reason = "use `sh_eprintln` or similar macros instead" }, ]

sh_*宏之所以不受该 lint 影响,是因为它们展开为对全局Shell实例(crates/common/src/io/shell.rs中的GLOBAL_SHELL: OnceLock<Mutex<Shell>>)的write!调用,而不是std::print*系列。

宏速查表

下表是文档给出的完整宏清单,逐一说明了通道、--quiet行为与用途:

通道--quiet抑制用途
sh_println!stdout是(目标:,见上文)命令的主要机器可读结果。
sh_print!stdout是(目标:,见上文)sh_println!,不带尾部换行。
sh_status!stderr状态文案("Compiling…"、"Deploying contract…")。
sh_progress!stderr是(stderr 非 tty 时也是 no-op)旋转指示器/进度条式的瞬时更新。
sh_warn!stderr可恢复的问题。添加 "Warning:" 前缀。
sh_err!stderr错误。添加 "Error:" 前缀。
sh_eprintln!stderr原始 stderr 文本的逃生舱(escape hatch)。
sh_eprint!stderrsh_eprintln!,不带尾部换行。
prompt!stderr(问题)+ stdin(答案)是(问题经sh_eprint!输出)交互式问答。

源码中的宏实现细节

在 crates/common/src/io/macros.rs 中可以印证这些宏的真实路由:

  • sh_err!/sh_warn!/sh_print!/sh_eprint!全部经由隐藏宏__sh_dispatch!分发到Shell::error/Shell::warn/print_out/print_err;分发时通过Shell::get()获取全局 shell,并刻意将全局锁的持有时间压到最短以避免嵌套调用死锁(源码注释明确说明这一点)。
  • sh_status!就是sh_eprintln!的别名($crate::sh_eprintln!($($args)*)),用于输出人类可读的诊断散文。
  • sh_progress!自带门控逻辑:只有当is_err_tty()!is_quiet()时才真正打印,并且始终返回Ok(())——进度输出是"尽力而为"的,永远不会让调用方失败:
#[macro_export] macro_rules! sh_progress { ($($args:tt)*) => {{ if $crate::shell::is_err_tty() && !$crate::shell::is_quiet() { let _ = $crate::sh_eprintln!($($args)*); } ::core::result::Result::<(), ::eyre::Report>::Ok(()) }}; }
  • prompt!的实现印证了"问题走 stderr、答案走 stdin"的推论:它先用sh_eprint!写出问题、flushstderr,再调用parse_line()从 stdin 读取(读取逻辑在 crates/common/src/io/stdin.rs,支持按行或按整段读取、自动剔除尾部\n/\r)。

Shell 包装器与输出模式

sh_*宏背后是 crates/common/src/io/shell.rs 中的Shell结构体,它记住三项全局偏好:

  • OutputModeNormal(默认)与Quiet两个取值;Quiet模式下warnprint_outprint_errprint全部变成 no-op,唯独error无条件输出。
  • OutputFormatText(默认)、JsonMarkdown,提供is_json()/is_markdown()供代码分支。
  • Verbosityu8级别的详细程度,对应 CLI 的-v/-vvv等。

此外Shell还通过ShellOut枚举支持三种底层写目标:Stream(带颜色的真实 stdout/stderr)、Empty(丢弃一切输出)、Captured(把 stdout/stderr 捕获到内存缓冲区,专供测试断言用)。color_choice()is_err_tty()等辅助函数则分别用于颜色与 tty 门控判断。

决策规则:每个sh_println!调用点都要回答的问题

文档为每一位编写或评审代码的开发者提供了一条可操作的决策规则。遇到任何sh_println!调用,依次问自己两个问题:

  1. 这是不是命令的规范主结果(canonical primary result)?
    • 是 → 保留sh_println!(stdout)。
    • 否 → 改用sh_status!sh_warn!sh_err!sh_eprintln!(stderr)。
  2. 这一行是否把标签和数据混在一起(例如"Deployer: 0x…")?
    • 标签属于散文 → 把整行挪到sh_status!(stderr)。
    • 只有数据属于 stdout → 只输出值本身(sh_println!)。
    • --json模式下 → 两者都应放进 stdout 上的同一个 JSON 文档内。

这条规则保证了 stdout 上永远不会出现 "标签 + 值" 的混排文本——那正是管道解析最容易踩的坑。

Per-command stdout 契约(目标状态)

下表是目标契约(target contract):后续的迁移 PR 会逐命令把当前行为对齐到这张表。它是"每个命令的 stdout 在迁移完成之后将包含什么"的权威依据,不一定是今天的真实行为。每行状态取值:

  • migrated—— 当前行为已符合该契约。
  • todo—— 当前行为尚未符合,需要后续 PR 跟进。

cast系列命令

命令文本模式 stdout--jsonstdout状态
cast call返回值(hex / 解码后)返回值的 JSONmigrated
cast send收据(--async时为 tx hash)JSON 收据(--async时为 hex tx hash)migrated
cast estimate燃料估算(十进制)JSON{ "gas": "…" }migrated
cast rpcRPC 结果(JSON)JSONmigrated
cast storage单个槽位的值布局的 JSONmigrated
cast logs美化打印的日志JSON 数组migrated
cast runTrace / 解码后的输出JSONmigrated
cast traceTraceJSON tracemigrated
cast wallet new每个钱包一行记录:address(keystore 模式)或address\tprivate_key(无 keystore 模式);当 stdout 是 tty 时省略输出(此时 stderr 散文已展示这些值)keystore 模式为{ address, public_key, path }数组;无 keystore 模式为{ address, public_key, private_key }数组migrated
cast wallet sign签名JSONmigrated
cast wallet sign-auth签名后的授权 RLPJSONmigrated
cast erc20 balance余额(十进制)JSON 字符串migrated
cast create2address\tsalt(制表符分隔);挖矿模式下,stdout 是 tty 时省略输出(stderr 散文已展示这些值)不适用migrated
cast access-list访问列表JSONmigrated
cast interfaceSolidity 接口源码JSON ABI 数组migrated
cast artifactJSON artifact不适用migrated
cast creation-codehex 字节码(--disassemble时为反汇编)不适用migrated
cast constructor-args每个构造参数一行不适用migrated
cast b2e-payloadJSON 执行负载不适用migrated
cast tx-poolJSONJSONmigrated
cast da-estimate燃料估算JSONmigrated
cast find-block区块号JSONmigrated
cast mktx签名 RLPJSONmigrated
cast batch-mktx签名 RLP(--raw-unsigned时为未签名 RLP)不适用migrated
cast batch-send收据(--async时为 tx hash)JSON 收据(--async时为 hex tx hash)migrated

值得注意的细节:cast wallet newcast create2引入了基于 tty 的门控——当 stdout 是终端(交互式场景)时,为避免与 stderr 上的散文重复,这两条命令会省略 stdout 记录;只有当 stdout 被重定向/管道化(非 tty)时才输出机器可读行。这正是 shell.rs 中is_out_tty()辅助函数的用途(注释明确指出"用于门控那些会与交互会话中 stderr 状态散文重复的机器可读 stdout 记录")。

forge系列命令

命令文本模式 stdout--jsonstdout状态
forge build(空)JSON 构建输出todo
forge test(空;退出码 = 通过/失败)JSON 测试结果;--junit时为 JUnit XMLtodo
forge create部署时输出Deployer:/Deployed to:/Transaction hash:行;dry-run 时输出Contract:/Transaction:/ABI:行。编译器输出可能先行出现(归入forge build跟踪)JSON{ deployer, deployedTo, transactionHash }(dry-run 为 JSON{ contract, transaction, abi }todo
forge inspect <field>仅该字段的值(artifact/output打印合约 artifact JSON)该字段的 JSONmigrated
forge install(空)(空)migrated
forge init(空)(空)migrated
forge update(空)(空)migrated
forge remove(空)(空)migrated
forge clone(空)(空)migrated
forge bind(空)(空)migrated
forge bind-json(空)或生成的路径JSONmigrated
forge flatten展平后的源码不适用migrated
forge fmt(空)或--check时的格式化源码不适用migrated
forge tree依赖树JSONmigrated
forge config配置 TOMLJSON 配置migrated
forge selectors选择器输出JSONmigrated
forge eip712(空)类型 JSONmigrated
forge geiger发现结果JSONmigrated
forge lint(空;发现结果走 stderr/退出码)JSON 发现结果migrated
forge snapshot--diff时为每测试差异行;--format table时为表格;否则(空)不适用migrated
forge coverage覆盖率表格或报告通过--report输出 JSON / LCOV 等todo
forge cache(空)或路径JSONmigrated
forge clean(空)不适用migrated
forge completions生成的 shell 补全脚本不适用migrated
forge doc(空)不适用migrated
forge soldeer直通到soldeercrate;foundry 不添加任何包装散文不适用migrated
forge remappings每个 remapping 一行不适用migrated
forge compiler编译器信息JSONmigrated
forge verify-contract提交时输出<guid-or-job-id>\t<url>;已验证则为空不适用migrated
forge verify-bytecode<type> code matched with status <kind>{ bytecode_type, match_type, message }的 JSON 数组migrated

anvilchiselscript

命令文本模式 stdout--jsonstdout状态
anvilBanner、账户、RPC URL 输出到 stderr不适用todo
chiselREPL 输出不适用todo
forge script模拟/广播结果JSONtodo

注意anvil一行明确把Banner、账户列表与 RPC URL 全部归入 stderr,这正是"stderr 承载一切诊断与散文"的典型体现。而表中未列出的命令目前尚未分类——文档明确要求:在依赖某个命令的 stdout 格式之前,先提交 issue 或 PR 对其进行分类。

编译报告器与进度输出的通道归属

契约特别点名了编译流程中的进度输出。foundry_common::compile(crates/common/src/compile.rs)在 TTY 模式下使用SpinnerReporter(定义于 crates/common/src/term.rs),其旋转指示器通过sh_eprint!写到stderr——源码注释直接写明:"Progress is a diagnostic, not data: write to stderr so stdout stays clean for machine-readable output."(进度是诊断信息而非数据:写入 stderr 以保持 stdout 对机器可读输出的纯净)。

同时,Spinner::tick只在stderr是终端时才工作(TermSettings::from_env依据std::io::stderr().is_terminal()决定indicate_progress),非 TTY 时自动变为 no-op。编译完成时报告器还会在 stderr 上补一个换行,避免后续消息覆盖之前的 tick。

不过文档也如实指出了当前的一个未完成项:非 TTY 场景下,编译报告器的回退实现(BasicStdoutReporter)目前仍然写到 stdout。把它翻转到 stderr 会一次性改变大量既有 snapshot 测试,因此被列入 per-command 迁移积压(backlog)。在 crates/common/src/compile.rs 的with_compilation_reporter_and_settings中可以看到这条回退路径的选择逻辑:quiet || is_json()时用NoReporter,否则 stderr 是 tty 用SpinnerReporter、不是 tty 用BasicStdoutReporter。对于一次性进度行,直接调用sh_progress!也是被允许的。

测试如何守护契约

契约不是纸面承诺,crates/common/src/io/macros.rs 内的单元测试把它固化为可执行断言:

  • routing_contract测试:使用Shell::captured()捕获输出,断言 stdout 只包含sh_print!/sh_println!产生的内容,而sh_eprint!/warn/error的内容全部落在 stderr,且断言"stdout 内容没有泄漏到 stderr"(assert!(!stderr.contains("out-print"), ...))。测试注释明确写着它断言"每个宏都路由到docs/dev/output-channels.md文档记录的通道"。
  • quiet_contract测试:把OutputMode设为Quiet后,断言 stdout 被抑制(stdout.is_empty())、sh_eprintln!/warn被抑制,但sh_err!(error)必然可见(assert!(stderr.contains("boom"), ...))。测试特意把"当前 stdout 会被--quiet抑制"这一过渡行为钉死(pinned),迫使未来翻转旁路的迁移必须同步更新该测试——这是工程上防止契约漂移的经典手法。

此外,forge命令层面对sh_*宏的广泛使用(在 crates/forge/src/cmd 下,buildcreateinspectselectorssnapshotbindconfigremappings等命令均有调用)也从调用侧印证了上表各命令的 stdout 形态。

总结与迁移路线

Foundry 的输出通道契约可以浓缩为一句话:stdout 只给机器,stderr 只给人。它通过三层机制落地:

  1. 规范层:本文档定义的契约、推论与 per-command stdout 对照表;
  2. 工具层foundry_common::iosh_*宏 + 全局Shell路由,以及 clippy.toml 中禁绝std::print*的工作区级 lint;
  3. 验证层Shell::captured()捕获式单元测试,把通道路由与--quiet语义固化为可执行断言。

当前cast全系列命令已全部migratedforge大部分命令已迁移,而forge build/forge test/forge create/forge coverage以及anvil/chisel/forge script仍处于todo状态——它们正是后续迁移 PR 的工作对象。对下游使用者而言,最稳妥的做法是:只依赖表中标为migrated的命令的 stdout 格式;对todo或未列出的命令,在自动化脚本中始终丢弃 stderr 并以--json作为主解析通道,同时留意--quiet当前仍会抑制 stdout 的过渡期行为。

参考链接

  • 契约文档:docs/dev/output-channels.md
  • 宏实现:crates/common/src/io/macros.rs
  • Shell 包装器:crates/common/src/io/shell.rs
  • 旋转指示器 / 进度:crates/common/src/term.rs
  • 编译报告器:crates/common/src/compile.rs
  • stdin 读取工具:crates/common/src/io/stdin.rs
  • 禁用宏 lint 配置:clippy.toml
  • 开发者文档索引:docs/dev/README.md

【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 2:56:38

Ventoy+deepin打造可靠Linux To Go工作流

1. 为什么“Linux to Go”不再是实验室玩具&#xff0c;而是真实工作流刚需我第一次把 deepin 装进 U 盘是在 2020 年底&#xff0c;当时用的是传统的dd方式写入 ISO&#xff0c;结果在三台不同品牌的笔记本上——一台戴尔 XPS、一台联想 ThinkPad T14、还有一台华硕 ROG 游戏本…

作者头像 李华
网站建设 2026/9/17 2:56:22

Redis为何不支持事务回滚?性能取舍与设计哲学深度解析

1. 问题背后的真实考点面试现场&#xff0c;当面试官抛出“为什么 Redis 不支持回滚&#xff1f;”这个问题时&#xff0c;很多人的第一反应是愣住。因为从直觉上讲&#xff0c;一个数据库不支持回滚&#xff0c;听起来像一个严重的功能缺陷——MySQL有ROLLBACK&#xff0c;Pos…

作者头像 李华
网站建设 2026/9/17 2:55:19

腾讯云部署OpenClaw:从选型到Skill安装的完整实操指南

上周帮朋友在腾讯云上把 OpenClaw 跑起来了&#xff0c;从买服务器到 Agent 能正常对话、装上第一个 Skill&#xff0c;前后确实没花几分钟。他自己也感慨&#xff0c;这玩意儿比想象中简单得多&#xff0c;真正花时间的反而是选模型、填 APIKey 这些"脑力活"。2026年…

作者头像 李华