在 F# 中使用 ANSI 转义码 —— printfn、\x1b、\027 是十进制非八进制、Spectre.Console
F# 字符串字面量原生支持 \x1b(F# 4.5+,2018 年 8 月)、\u001b(Unicode 码点 —— 所有 F# 版本包括 .NET Framework F# 2.0 都可用),以及 \NNN 三元组 —— 但 F# 的 \NNN 是十进制,不是八进制(与 C# / VB.NET / PowerShell 共享的 .NET 运行时怪异,与 C / Erlang / Java 中 \033 是八进制 27 相反)。可移植性陷阱:把 "\033[31m" 从 C / Erlang / Bash 片段复制到 F# 中得到的是字节 33(= !),不是 ESC。请使用 "\x1b"(F# 4.5+)或 "\027"(十进制三元组,全版本)或 "\u001b"(Unicode 码点,全版本)—— 永远不要写 "\033"。
规范的 F# 单行写法是 printfn "\x1b[1;31merror:\x1b[0m %s" msg —— printf / printfn 是 F# 的类型检查 printf 系列(格式字符串在编译时解析,与 C# / Java 在运行时校验格式字符串相反)。%s 插入字符串,%d 插入整数,%A 通过结构化美化打印任意值。嵌在格式字符串中的转义字节原样传递给 System.Console.Out。printfn 的换行符是平台相关的(Windows 上 \r\n、Unix 上 \n)—— 增量构建单行时请使用纯 printf。
需要比临时转义更丰富时,Spectre.Console 是事实标准的现代 .NET 控制台工具包 —— F# 中用法完全相同(AnsiConsole.MarkupLine "[bold red]error:[/] permission denied")。配合 Argu(F# 规范的 CLI 解析器 —— 自动着色的 --help 输出,通过判别联合提供类型安全的参数记录)即可获得完整的 F# 脚本人体工学。F# 风格的模板化彩色输出选 BlackFox.ColoredPrintf —— cprintfn "$red[error:] %s" msg 使用 $colour[...] 模板语法,编译为标准 ANSI。F# 构建脚本(FAKE)选 Fake.Core.Trace 模块,发出着色的步骤标题、成功 / 失败标记,以及 FAKE DSL 原生的结构化日志。
F# Interactive (FSI) REPL 注意(本页主要 SERP 差异化):从普通终端提示符运行的 dotnet fsi 解析 printfn 输出中的 ANSI —— 颜色按预期工作。但在 Visual Studio 的 *F# Interactive* 工具窗口和 VS Code Ionide 的 *F# Interactive* 输出窗格内部,ANSI 转义字节被字面渲染为 \x1b[31m... 文本 —— 这些窗格是文本控件,不经过 VT 终端模拟器分发。修复方法:通过 not Console.IsOutputRedirected && not (isNull (Environment.GetEnvironmentVariable "VSAPPIDDIR")) 检测(Visual Studio 设置 VSAPPIDDIR;Ionide / VS Code 设置 VSCODE_PID 和 TERM_PROGRAM=vscode)从而在 IDE 内嵌 FSI 会话中跳过 ANSI,或在 FSI 启动配置中设置 NO_COLOR=1。
能力门控:System.Console.IsOutputRedirected(管道 / 重定向时为 true —— 跳过颜色以避免污染文件)、System.Environment.GetEnvironmentVariable("NO_COLOR")(通用退出)、System.Console.OutputEncoding <- System.Text.Encoding.UTF8(制表符 + 表情符号安全 —— 不同 OS / 区域设置下默认值不同)。
推荐库
- Spectre.Console
事实标准的现代 .NET 控制台工具包 —— F# 中用法完全相同。
AnsiConsole.MarkupLine "[bold red]error:[/] permission denied"、表格、树、进度、提示符、异常渲染。自动检测终端能力(TrueColor / 256 / 16 / 无色)并优雅降级。F# 管道符(|>)与 Spectre 的 builder API 干净地组合。 - BlackFox.ColoredPrintf
F# 风格的彩色 printf ——
cprintfn "Hello $red[%s], your build $green[succeeded] in $yellow[%d]ms" name ms。$colour[...]模板语法用匹配的 SGR 序列 + reset 包裹内部文本。编译为标准 ANSI 字节;当 stdout 非 TTY 时通过System.Console.IsOutputRedirected自动禁用。轻量(一个 NuGet 包、约 200 行)—— 适合需要模板级颜色表达式但不想要 Spectre 完整 TUI 机制的场景。 - Argu
F# 规范的 CLI 解析器 —— 通过判别联合提供类型安全的参数记录。
type Args = | Verbose | Port of int | Config of string自动生成--verbose、--port 8080、--config FILE,以及着色的--help输出(章节标题粗体、选项名青色、描述默认色 —— 能力门控)。任何非平凡参数的 F# 脚本 / 工具的正确选择;在 F# 社区以及 FAKE / Paket / Fable 自身中广泛使用。 - Fake.Core.Trace
FAKE 构建脚本 trace 模块 ——
Trace.trace、Trace.traceImportant、Trace.traceError、Trace.traceSuccessfulTarget发出针对构建输出调校的着色步骤标题和状态标记。Trace.useWith用开始 / 结束着色日志包裹一段代码块。自动能力门控(CI 中TERM=dumb或NO_COLOR=1时无色)。在 FAKE 5+ 下编写build.fsx脚本的惯用选择。
常用写法
// F# accepts THREE useful ESC forms in "..." literals.
// All compile to the same single byte (0x1b = 27).
//
// "\x1b" — hex (F# 4.5+ / August 2018)
// "\u001b" — Unicode codepoint (every F# version, even F# 2.0)
// "\027" — \NNN is DECIMAL in F# (.NET runtime convention,
// shared with C# / VB.NET / PowerShell)
//
// THE PORTABILITY TRAP:
// "\033" — DOES NOT WORK as ESC in F#!
// \033 in F# is decimal 33 (= byte 0x21 = "!").
// In C / Erlang / Java, \033 is octal 27 (= ESC).
// Copy-pasting from those languages silently breaks.
//
// When porting from C / Erlang / Bash, replace every "\033"
// with "\x1b" (or "\027" if you need pre-F#-4.5 compatibility).
module AnsiDemo
open System
open System.Text
[<EntryPoint>]
let main _ =
// Box-drawing + emoji safety — defaults vary by OS / culture.
Console.OutputEncoding <- Encoding.UTF8
// All three forms produce the same red "error:" output:
printfn "\x1b[1;31merror:\x1b[0m permission denied"
printfn "\u001b[1;31merror:\u001b[0m permission denied"
printfn "\027[1;31merror:\027[0m permission denied"
// Truecolor — 38;2;R;G;B
printfn "\x1b[38;2;255;128;0morange truecolor\x1b[0m"
// 256-palette — 38;5;n
printfn "\x1b[38;5;196mbright red\x1b[0m"
// F# printf is TYPE-CHECKED at compile time. The format
// string is parsed by the compiler, not at runtime:
let msg = "permission denied"
printfn "\x1b[1;31merror:\x1b[0m %s" msg
let tests, failed = 142, 3
printfn "\x1b[32mrun:\x1b[0m %d tests, \x1b[31m%d failed\x1b[0m" tests failed
// Reusable helper — pure F#:
let esc (sgr: string) (text: string) =
sprintf "\x1b[%sm%s\x1b[0m" sgr text
printfn "%s" (esc "32" "OK")
printfn "%s" (esc "1;31" "FAIL")
0// dotnet add package Spectre.Console
open Spectre.Console
// AnsiConsole.MarkupLine works identically from F#:
AnsiConsole.MarkupLine "[bold red]error:[/] permission denied"
AnsiConsole.MarkupLine "[yellow]warn:[/] deprecated flag"
AnsiConsole.MarkupLine "[green]ok:[/] 142 tests passed"
// Truecolor — RGB triplet or hex:
AnsiConsole.MarkupLine "[#ff8000]orange truecolor[/]"
AnsiConsole.MarkupLine "[rgb(255,128,0)]same, RGB form[/]"
// F# pipeline (`|>`) composes cleanly with Spectre builders:
let table =
Table()
|> fun t -> t.AddColumns("File", "Status")
|> fun t -> t.AddRow("app.log", "[green]ok[/]")
|> fun t -> t.AddRow("db.log", "[red]missing[/]")
AnsiConsole.Write table
// Spectre's status spinner — async-friendly via F# computation:
AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.Start("Compiling…", fun ctx ->
System.Threading.Thread.Sleep 500
ctx.Status <- "[yellow]Type-checking…[/]"
System.Threading.Thread.Sleep 500
)
// Exception rendering — coloured stack frames out of the box:
try
raise (System.InvalidOperationException "demo")
with ex ->
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything)$red[...] 语法// dotnet add package BlackFox.ColoredPrintf
open BlackFox.ColoredPrintf
// $colour[...] template syntax wraps inner text with SGR + reset.
// Compiles to standard ANSI bytes; auto-disables when stdout
// is not a TTY (via System.Console.IsOutputRedirected).
cprintfn "$red[error:] permission denied"
cprintfn "$yellow[warn:] deprecated flag"
cprintfn "$green[ok:] 142 tests passed"
// Nest %s / %d / %A inside the template — type-checked at
// compile time like every F# printf:
let name, ms = "build", 1240
cprintfn "Hello $red[%s], your $green[succeeded] in $yellow[%d]ms" name ms
// Multiple colours per line — read top-to-bottom like prose:
let port = 8080
cprintfn "$cyan[[INFO]] started on port $magenta[%d]" port
let tests, failed = 142, 3
cprintfn "$green[run:] %d tests, $red[%d failed]" tests failed
// Available colours: red green yellow blue magenta cyan white
// black + their "bright" / "dark" variants
// e.g. $darkred[...] $brightcyan[...]
cprintfn "$darkred[fatal:] $brightyellow[stack overflow]"
// Plain Console.Out is the destination — pipe through a file
// and the colour template auto-elides:
// dotnet run > out.txt # plain text (no escape bytes)
// dotnet run # ANSI in terminal
// NO_COLOR=1 dotnet run # plain text (env opt-out)
()// Capability gating in F#:
// System.Console.IsOutputRedirected — true when piped
// System.Environment.GetEnvironmentVariable("NO_COLOR")
// System.Environment.GetEnvironmentVariable("FORCE_COLOR")
//
// THE FSI REPL CAVEAT (page's primary SERP differentiator):
// `dotnet fsi` from a normal terminal prompt parses ANSI in
// printfn output — colours work as expected. BUT inside
// Visual Studio's *F# Interactive* tool window AND inside
// the VS Code Ionide *F# Interactive* output pane, ANSI
// escape bytes are rendered LITERALLY as "\x1b[31m..." text
// — those panes are text widgets that don't dispatch
// through a VT terminal emulator.
//
// Detect IDE-hosted FSI sessions and skip colour:
open System
let isIdeHostedFsi () =
// Visual Studio sets VSAPPIDDIR.
// Ionide / VS Code sets VSCODE_PID + TERM_PROGRAM=vscode.
not (isNull (Environment.GetEnvironmentVariable "VSAPPIDDIR"))
|| (Environment.GetEnvironmentVariable "TERM_PROGRAM" = "vscode"
&& not (isNull (Environment.GetEnvironmentVariable "VSCODE_PID")))
let ansiCapable () =
// The Unix-standard kill switch — honour first.
if not (isNull (Environment.GetEnvironmentVariable "NO_COLOR"))
then false
// Explicit opt-in overrides redirection.
elif not (isNull (Environment.GetEnvironmentVariable "FORCE_COLOR"))
then true
// Piped / redirected output — don't poison logs.
elif Console.IsOutputRedirected then false
// IDE-hosted FSI panes render escapes literally.
elif isIdeHostedFsi () then false
else true
let style (sgr: string) (text: string) =
if ansiCapable ()
then sprintf "\x1b[%sm%s\x1b[0m" sgr text
else text
printfn "%s" (style "32" "OK")
printfn "%s" (style "1;31" "FAIL")
// FAKE build-script tracing — capability-gated automatically:
//
// #r "nuget: Fake.Core.Trace"
// open Fake.Core
//
// Trace.trace "starting build"
// Trace.traceImportant "compiling Foo.fsproj"
// Trace.traceError "test failed: BarSpec.fs:42"
// Trace.traceSuccessfulTarget "Build" :: []
// |> ignore
//
// Fake.Core.Trace honours NO_COLOR and TERM=dumb out of the
// box — no manual gating needed inside build.fsx.