跳到主要内容
ansicode
OCaml

在 OCaml 中使用 ANSI 转义码 —— \027、ANSITerminal、Fmt + Logs

OCaml 字符串字面量支持 `"\027"`(十进制 —— 规范的 OCaml ESC 形式,在 OCaml 1.0 起的所有版本中可用)、`"\x1b"`(十六进制,OCaml 4.06+)以及 `"\o033"`(八进制,OCaml 4.06+)。**OCaml 的妙处**:`"\NNN"` 是**十进制**,不像 C 那样是八进制 —— Nim 用户踩到的同一个陷阱。C 中 `"\033"` 是字节 27(八进制);OCaml 中 `"\033"` 是字节 33(十进制 —— 字符 `!`)。从 C 移植时规范修复是 `"\027"`(十进制)或 —— 在 OCaml 4.06+ —— `"\x1b"`(十六进制)或 `"\o033"`(八进制,当你想保留字面看起来一样时)。没有 `\e` 命名转义。`print_string "\027[1;31merror:\027[0m permission denied\n"` 通过运行时的 `stdout` 通道写原始字节,在 macOS、Linux、BSD、Windows 10+ Conhost 1709+ 上原生解析。 选 **`ANSITerminal`**(opam 包,OCaml 规范 ANSI 库)—— `ANSITerminal.print_string [red; Bold] "error: "` 读作一个样式列表后跟文本,自动发出 reset,在 Windows 上通过原生 console-mode 切换处理能力检测。**结构化 / 格式化输出**选 **`fmt`**(Dune 默认格式化器)—— `Fmt.pr "@{<red>error@}: %s@." msg` 把语义标记标签解析为 SGR 序列并尊重 `Fmt.set_style_renderer Format.std_formatter `Ansi_tty`。将 `fmt` 与 **`logs`** 配对做结构化日志,每个来源 / 级别带彩色前缀(`Logs.info (fun m -> m "started on %d" port)` 在 stdout 是 TTY 时产生彩色 `[INFO]` 前缀)。完整 TUI 运行时选 **`notty`** —— pqwy 的高质量声明式 TUI 库,带图像组合(`I.string A.(fg red) "error"` 返回 `Notty.image`,用 `<|>` / `<->` 组合,经 `Notty_unix.Term` 渲染)。 能力门控:`Unix.isatty Unix.stdout`(`unix` 库,随多数 OCaml 安装一起)在 stdout 重定向时返回 `false`。搭配 `Sys.getenv_opt "NO_COLOR"` 处理通用退出。**utop REPL 注意**(本页主要 SERP 差异化):utop 内的 `print_string "\027[31mfoo\027[0m"` **确实**渲染红色(utop 将 stdout 管道到底层 lambdaterm 解析 ANSI)。然而当 REPL 显示顶层绑定的**值**(`let s = "\027[31mfoo\027[0m";;`),utop 把 OCaml 字符串字面量原样回显给你 —— 转义字节作为 `\027[...]` 字面显示,而非颜色。修复方法:显式打印字符串(`let () = print_string s`)而非让 utop 回显值,或安装 `utop-full` 并使用 `UTop.set_phrase_terminator` / `UTop.set_show_box` 定制 REPL 渲染。

推荐库

  • ANSITerminal

    规范的 OCaml ANSI 库 —— `opam install ansiterminal`。`ANSITerminal.print_string [red; Bold] "error: "` 读作样式列表 + 文本,自动发出 reset,支持 `[Foreground Red; Background Yellow; Bold; Underlined; Inverse]` 加上 24 位 `[Foreground (RGB (255, 128, 0))]`。模块级 `ANSITerminal.set_autoreset true` 确保每次 print 都 reset。在 Windows 上:原生 console-mode 切换 —— 不需要手动 VT 启用。几乎每个 OCaml CLI 工具最终都会引入的一个库。

  • fmt

    Dune 默认格式化器 —— 现代 OCaml 中 `Fmt.pr` / `Fmt.epr` 替代直接 `Format.printf` 用于面向人的输出。语义标记标签(`@{<red>error@}`、`@{<bold>fatal@}`)在 `Fmt.set_style_renderer Format.std_formatter `Ansi_tty` 时解析为 SGR 序列。切到 `\`None` 得到纯文本输出(CI / 管道)。需要格式化字符串驱动的彩色输出而非命令式样式列表调用时选它。

  • logs

    结构化日志 —— 与 `fmt` 配对实现按源 / 级别的彩色前缀。`Logs.info (fun m -> m "started on %d" port)` 产生 `[INFO] started on 8080`,stdout 是 TTY 时 `[INFO]` 渲染为绿色。源(`Logs.Src.create "db" ~doc:"database"`)让你通过 `LOGS_REPORTER` / `LOGS_LEVEL` env 按模块过滤。规范的 OCaml 结构化日志选择;在整个现代 OCaml 生态(cohttp、dune、mirage)中使用。

  • notty

    pqwy 的高质量声明式 TUI 库 —— OCaml 对 vty / blessed / brick 的回答。`I.string A.(fg red) "error"` 返回 `Notty.image` 值,用 `<|>`(水平)和 `<->`(垂直)组合。经 `Notty_unix.Term.create ()` 渲染做 unix 风格 stdout 驱动循环,或经 `Notty_lwt.Term.create ()` 做 Lwt 集成的异步友好事件循环。需要重绘布局而非仅一行彩色日志时选它。

常用写法

直接 print_string —— \NNN-是-十进制 OCaml 妙处 + 4.06+ 支持 \x1b
(* OCaml accepts three ESC forms in "..." literals.
   All compile to the same single byte (0x1b = 27).

     "\027"   — \NNN is DECIMAL (OCaml 1.0+, canonical)
     "\x1b"  — hex (OCaml 4.06+)
     "\o033" — octal (OCaml 4.06+)

   THE OCAML TWIST: \NNN is DECIMAL — not octal like C.
   In C, "\033" is byte 27 (octal 33 = decimal 27 = ESC).
   In OCaml, "\033" is byte 33 (decimal — the '!' character).
   Porting from C? "\033" silently mis-compiles. The fix is
   "\027" (decimal) or — on OCaml 4.06+ — "\x1b" (hex) or
   "\o033" (octal, when you want the literal looking the same).
   There is no \e named escape.

   On OCaml < 4.06 you have ONLY \NNN decimal — must use "\027". *)

let () =
  print_string "\027[1;31merror:\027[0m permission denied\n";
  print_string "\027[33mwarn:\027[0m deprecated flag\n";
  print_string "\x1b[32mok:\x1b[0m 142 tests passed\n";        (* 4.06+ *)
  print_string "\o033[36minfo:\o033[0m skipping cache\n";       (* 4.06+ *)

  (* Truecolor — 38;2;R;G;B *)
  print_string "\027[38;2;255;128;0morange truecolor\027[0m\n";

  (* 256-palette — 38;5;n *)
  print_string "\027[38;5;196mbright red\027[0m\n"

(* Reusable helper — no opam dependency: *)
let esc sgr text = "\027[" ^ sgr ^ "m" ^ text ^ "\027[0m"

let () =
  print_endline (esc "1;31" "FATAL" ^ " server crashed");
  print_endline (esc "32"   "ok:"    ^ " 142 tests passed");
  print_endline (esc "38;2;255;128;0" "truecolor orange")
ANSITerminal —— 样式列表 API、autoreset、RGB truecolor
(* opam install ansiterminal
   dune file:
     (executable (name main) (libraries ansiterminal)) *)

open ANSITerminal

let () =
  set_autoreset true;  (* every print auto-resets — recommended *)

  (* Style list + text — the canonical ANSITerminal call: *)
  print_string [red; Bold] "error: ";
  print_endline "permission denied";

  print_string [green] "ok: ";
  print_endline "142 tests passed";

  print_string [Background Yellow; black; Bold] " CAUTION ";
  print_endline " brakes wet";

  (* 24-bit truecolor — Foreground (RGB (r, g, b)): *)
  print_string [Foreground (RGB (255, 128, 0))] "hot orange";
  print_endline "";

  print_string
    [ Foreground (RGB (0, 110, 130))
    ; Background (RGB (240, 240, 240))
    ; Bold ]
    " deep teal on light grey ";
  print_endline "";

  (* Cursor + screen helpers — same module: *)
  erase Eol;            (* erase to end of line *)
  set_cursor 1 1;       (* move to (col, row) *)
  save_cursor ();
  restore_cursor ();

  (* Capability check — ANSITerminal handles Windows via
     native console mode toggling, but you can also check
     manually: *)
  if Unix.isatty Unix.stdout
  then prerr_endline "stdout is a TTY"
  else prerr_endline "stdout redirected — no colour"
Fmt + Logs —— Dune 级结构化彩色输出
(* opam install fmt logs
   dune file:
     (executable
       (name main)
       (libraries fmt fmt.tty logs logs.fmt logs.cli)) *)

(* ─── Fmt — semantic markup tags compile to SGR ─── *)
let () =
  (* Enable ANSI rendering on stdout / stderr formatters.
     Switch to `None for CI / piped output. *)
  Fmt.set_style_renderer Fmt.stdout `Ansi_tty;
  Fmt.set_style_renderer Fmt.stderr `Ansi_tty;

  Fmt.pr "@{<red>error:@} %s@." "permission denied";
  Fmt.pr "@{<yellow>warn:@} %s@." "deprecated flag";
  Fmt.pr "@{<green>ok:@} 142 tests passed@.";
  Fmt.pr "@{<bold>@{<magenta>FATAL@}@}: server crashed@.";

  (* Compose markup with format directives: *)
  Fmt.pr "@{<cyan>request@} %s %s in @{<bold>%dms@}@."
    "GET" "/users/42" 142;

  (* Custom styled printer — reusable: *)
  let pp_status ppf = function
    | `Ok    -> Fmt.pf ppf "@{<green>OK@}"
    | `Fail  -> Fmt.pf ppf "@{<red>FAIL@}"
    | `Skip  -> Fmt.pf ppf "@{<yellow>SKIP@}"
  in
  Fmt.pr "[%a] migration applied@." pp_status `Ok

(* ─── Logs — structured logging with coloured levels ─── *)
let () =
  Logs.set_reporter (Logs_fmt.reporter ());
  Logs.set_level (Some Logs.Info);

  Logs.info  (fun m -> m "started on port %d" 8080);
  Logs.warn  (fun m -> m "slow query: %dms" 1240);
  Logs.err   (fun m -> m "auth failed for user %d" 42);

  (* Per-module log source — filter via LOGS_LEVEL=db:debug *)
  let src = Logs.Src.create "db" ~doc:"database layer" in
  let module Db = (val Logs.src_log src : Logs.LOG) in
  Db.info (fun m -> m "connected to %s" "postgres://...")
能力门控 + utop REPL 值回显注意 + Notty TUI
(* Capability gating in OCaml:
     Unix.isatty Unix.stdout    — stdlib (via unix lib)
     Sys.getenv_opt "NO_COLOR"  — universal opt-out
     Sys.getenv_opt "FORCE_COLOR" — universal opt-in

   utop REPL CAVEAT (page's primary SERP differentiator):
     print_string "\027[31mfoo\027[0m"  → renders RED in utop
       (utop pipes stdout to lambdaterm which parses ANSI)
     let s = "\027[31mfoo\027[0m";;        → utop echoes the
       OCaml string LITERAL: escape bytes show as \027[...] —
       NOT as a colour. utop renders the VALUE, not the meaning.

   Fix: print explicitly, don't let utop echo the value:
     let () = print_string s
   Or use UTop.set_phrase_terminator / UTop.set_show_box to
   customise REPL rendering (utop-full required). *)

let ansi_capable () =
  match Sys.getenv_opt "NO_COLOR", Sys.getenv_opt "FORCE_COLOR" with
  | Some _, _ -> false
  | _, Some _ -> true
  | _         -> Unix.isatty Unix.stdout

let styled text sgr =
  if ansi_capable ()
  then Printf.sprintf "\027[%sm%s\027[0m" sgr text
  else text

let () =
  print_endline (styled "OK"   "32");
  print_endline (styled "FAIL" "1;31")

(* ─── Notty — declarative TUI image composition ─── *)
(* opam install notty
   dune: (executable (name tui) (libraries notty notty.unix)) *)

let tui_demo () =
  let open Notty in
  let open Notty.Infix in

  let title  = I.string A.(fg yellow ++ st bold) "notty demo" in
  let ok     = I.string A.(fg green)  "✓ ok  " <|>
               I.string A.empty       "142 tests passed" in
  let fail   = I.string A.(fg red)    "✗ FAIL" <|>
               I.string A.empty       " 3 tests failed" in
  let info   = I.string A.(fg cyan)   "ℹ info" <|>
               I.string A.empty       " skipping cache" in

  let body = ok <-> fail <-> info in
  let img  = title <-> I.void 1 1 <-> body in

  let term = Notty_unix.Term.create () in
  Notty_unix.Term.image term img;
  ignore (Notty_unix.Term.event term);
  Notty_unix.Term.release term

let () = tui_demo ()

相关序列

其他语言