跳到主要内容
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.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` 脚本的惯用选择。

常用写法

直接 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.

相关序列

其他语言