跳到主要内容
ansicode
F#

在 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.Outprintfn 的换行符是平台相关的(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_PIDTERM_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.traceTrace.traceImportantTrace.traceErrorTrace.traceSuccessfulTarget 发出针对构建输出调校的着色步骤标题和状态标记。Trace.useWith 用开始 / 结束着色日志包裹一段代码块。自动能力门控(CI 中 TERM=dumbNO_COLOR=1 时无色)。在 FAKE 5+ 下编写 build.fsx 脚本的惯用选择。

常用写法

直接 printfn —— \x1b、\u001b 以及 \NNN 十进制陷阱
// 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
Spectre.Console —— F# 管道符与标记
// 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)
BlackFox.ColoredPrintf —— F# 模板化 $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)
()
能力门控 + F# Interactive (FSI) REPL 注意
// 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.

相关序列

其他语言