拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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

Foundry 输出通道契约Output Channelsstdout/stderr 分离规范与 sh_* 宏实践指南【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundryFoundry 作为一个由forge、cast、anvil、chisel、script、verify等多个 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契约原文给出了五条推论它们界定了各输出选项与通道之间的边界--json只改变 stdout 的格式绝不改变通道的纯净度——它不会把诊断信息挪进 stdout也不会让 stdout 变脏。--quiet抑制 stderr 诊断与进度输出。目标契约要求它永远不改变 stdout 内容但当前实现中sh_print!/sh_println!仍会被--quiet抑制见 macros.rs 中print_out的 TODO 注释这一旁路bypass将在forge/script中主要的散文式 stdout 调用点迁移到sh_status!之后翻转。sh_err!是文档明确列出的例外致命错误永远输出到 stderr且不受--quiet抑制。-vvv增加 stderr 的详细程度但它绝不能改变 stdout 内容。没有主结果的命令例如forge install默认不向 stdout 写入任何内容。提示语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: OnceLockMutexShell的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!stderr是同sh_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模式下warn、print_out、print_err、print全部变成 no-op唯独error无条件输出。OutputFormatText默认、Json、Markdown提供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!调用依次问自己两个问题这是不是命令的规范主结果canonical primary result是 → 保留sh_println!stdout。否 → 改用sh_status!、sh_warn!、sh_err!或sh_eprintln!stderr。这一行是否把标签和数据混在一起例如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 / 解码后返回值的 JSONmigratedcast send收据--async时为 tx hashJSON 收据--async时为 hex tx hashmigratedcast estimate燃料估算十进制JSON{ gas: … }migratedcast rpcRPC 结果JSONJSONmigratedcast storage单个槽位的值布局的 JSONmigratedcast logs美化打印的日志JSON 数组migratedcast runTrace / 解码后的输出JSONmigratedcast traceTraceJSON tracemigratedcast wallet new每个钱包一行记录addresskeystore 模式或address\tprivate_key无 keystore 模式当 stdout 是 tty 时省略输出此时 stderr 散文已展示这些值keystore 模式为{ address, public_key, path }数组无 keystore 模式为{ address, public_key, private_key }数组migratedcast wallet sign签名JSONmigratedcast wallet sign-auth签名后的授权 RLPJSONmigratedcast erc20 balance余额十进制JSON 字符串migratedcast create2address\tsalt制表符分隔挖矿模式下stdout 是 tty 时省略输出stderr 散文已展示这些值不适用migratedcast access-list访问列表JSONmigratedcast interfaceSolidity 接口源码JSON ABI 数组migratedcast artifactJSON artifact不适用migratedcast creation-codehex 字节码--disassemble时为反汇编不适用migratedcast constructor-args每个构造参数一行不适用migratedcast b2e-payloadJSON 执行负载不适用migratedcast tx-poolJSONJSONmigratedcast da-estimate燃料估算JSONmigratedcast find-block区块号JSONmigratedcast mktx签名 RLPJSONmigratedcast batch-mktx签名 RLP--raw-unsigned时为未签名 RLP不适用migratedcast batch-send收据--async时为 tx hashJSON 收据--async时为 hex tx hashmigrated值得注意的细节cast wallet new与cast create2引入了基于 tty 的门控——当 stdout 是终端交互式场景时为避免与 stderr 上的散文重复这两条命令会省略 stdout 记录只有当 stdout 被重定向/管道化非 tty时才输出机器可读行。这正是 shell.rs 中is_out_tty()辅助函数的用途注释明确指出用于门控那些会与交互会话中 stderr 状态散文重复的机器可读 stdout 记录。forge系列命令命令文本模式 stdout--jsonstdout状态forge build空JSON 构建输出todoforge test空退出码 通过/失败JSON 测试结果--junit时为 JUnit XMLtodoforge create部署时输出Deployer:/Deployed to:/Transaction hash:行dry-run 时输出Contract:/Transaction:/ABI:行。编译器输出可能先行出现归入forge build跟踪JSON{ deployer, deployedTo, transactionHash }dry-run 为 JSON{ contract, transaction, abi }todoforge inspect field仅该字段的值artifact/output打印合约 artifact JSON该字段的 JSONmigratedforge install空空migratedforge init空空migratedforge update空空migratedforge remove空空migratedforge clone空空migratedforge bind空空migratedforge bind-json空或生成的路径JSONmigratedforge flatten展平后的源码不适用migratedforge fmt空或--check时的格式化源码不适用migratedforge tree依赖树JSONmigratedforge config配置 TOMLJSON 配置migratedforge selectors选择器输出JSONmigratedforge eip712空类型 JSONmigratedforge geiger发现结果JSONmigratedforge lint空发现结果走 stderr/退出码JSON 发现结果migratedforge snapshot--diff时为每测试差异行--format table时为表格否则空不适用migratedforge coverage覆盖率表格或报告通过--report输出 JSON / LCOV 等todoforge cache空或路径JSONmigratedforge clean空不适用migratedforge completions生成的 shell 补全脚本不适用migratedforge doc空不适用migratedforge soldeer直通到soldeercratefoundry 不添加任何包装散文不适用migratedforge remappings每个 remapping 一行不适用migratedforge compiler编译器信息JSONmigratedforge verify-contract提交时输出guid-or-job-id\turl已验证则为空不适用migratedforge verify-bytecodetype code matched with status kind行{ bytecode_type, match_type, message }的 JSON 数组migratedanvil、chisel、script命令文本模式 stdout--jsonstdout状态anvilBanner、账户、RPC URL 输出到 stderr不适用todochiselREPL 输出不适用todoforge script模拟/广播结果JSONtodo注意anvil一行明确把Banner、账户列表与 RPC URL 全部归入 stderr这正是stderr 承载一切诊断与散文的典型体现。而表中未列出的命令目前尚未分类——文档明确要求在依赖某个命令的 stdout 格式之前先提交 issue 或 PR 对其进行分类。编译报告器与进度输出的通道归属契约特别点名了编译流程中的进度输出。foundry_common::compilecrates/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 内容没有泄漏到 stderrassert!(!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 下build、create、inspect、selectors、snapshot、bind、config、remappings等命令均有调用也从调用侧印证了上表各命令的 stdout 形态。总结与迁移路线Foundry 的输出通道契约可以浓缩为一句话stdout 只给机器stderr 只给人。它通过三层机制落地规范层本文档定义的契约、推论与 per-command stdout 对照表工具层foundry_common::io的sh_*宏 全局Shell路由以及 clippy.toml 中禁绝std::print*的工作区级 lint验证层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.rsShell 包装器crates/common/src/io/shell.rs旋转指示器 / 进度crates/common/src/term.rs编译报告器crates/common/src/compile.rsstdin 读取工具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),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门