跳到主要内容
ansicode
Scala

在 Scala 中使用 ANSI 转义码 —— \u001B、fansi、scala.io.AnsiColor

Scala 字符串字面量**不**支持 `\e` 或 `\x1b` —— 与 Java、Kotlin 相同的约束。可移植的写法是 Unicode 转义 `\u001B`,在 Scala 2(自 2.0 起)与 Scala 3 上都可用。Scala 2 还接受八进制 `\033`,但 Scala 3 从字符串字面量中移除了八进制转义,因此希望在所有现代 Scala 上编译就写 `"\u001B[31m"`。`println("\u001B[1;31merror:\u001B[0m permission denied")` 在 JVM ≥ 8 的所有版本、以及 macOS、Linux、BSD、Windows 10+ 的 Conhost 1709+ / Windows Terminal(均原生解析 ANSI)上工作。 惯用辅助选 **`fansi`**(`com.lihaoyi::fansi`)—— Li Haoyi 的标准 Scala ANSI 库,被 Ammonite、mill、Scalafix、scalafmt 以及现代 Scala 生态大量采用。`fansi.Color.Red("error")` 返回带结构化颜色 span 的 `fansi.Str` 值(而非嵌入字节);`++` 拼接 span,`.overlay(fansi.Bold.On, 0, 5)` 在子区间上叠加属性,`.render` 物化为字节字符串。**`scala.io.AnsiColor`** 是标准库 mixin —— `Console.RED + "error" + Console.RESET` 风格的常量,便于在不引入第三方库的情况下输出一次性字节。**`pprint`**(同样 Li Haoyi)是标准的彩色漂亮打印器 —— `pprintln(value)` 输出缩进、fansi 着色的结果,Ammonite REPL 把它作为默认的 `println` 替换。需要更丰富的终端能力(行编辑、原始模式、能力探测)选 **JLine 3** —— Scala REPL 自身使用的 JVM 终端抽象。 能力门控:`System.console() != null` 是 JVM 可移植的检查(当 stdin/stdout 被重定向时返回 `null`)。搭配 `sys.env.get("NO_COLOR")` 处理 env 约定。**SBT 注意**:SBT 包装构建的 stdout 以注入自己的进度 UI,子进程的 ANSI 输出有时丢失颜色 —— 在 SBT 命令行加上 `-Dsbt.color=always`,或在 `build.sbt` 中设置 `ThisBuild / outputStrategy := Some(StdoutOutput)`,再启动需要真实颜色直通的脚本。`scala-cli` 与 `mill` 没有这个问题。

推荐库

  • fansi

    Li Haoyi 的标准 Scala ANSI 库 —— 被 Ammonite、mill、Scalafix、scalafmt 采用。`fansi.Color.Red("x")` 返回结构化 `fansi.Str`(颜色 span 而非原始字节);`++` 拼接,`.overlay(fansi.Bold.On, 0, 5)` 在子区间上叠加属性,`.render` 物化为 ANSI 字节字符串,`fansi.Str.ansiRegex.replaceAllIn(s, "")` 是 Scala 标准的日志清洗器。

  • scala.io.AnsiColor (stdlib)

    Scala 标准库 mixin —— 预定义字符串常量 `Console.RED` / `GREEN` / `YELLOW` / `BLUE` / `MAGENTA` / `CYAN` / `WHITE`(前景)、`RED_B` 等(背景)、`BOLD` / `UNDERLINED` / `BLINK` / `REVERSED`、以及 `RESET`。在不引入第三方库的情况下做一次性彩色输出。没有组合原语 —— 只能字符串拼接。

  • pprint

    彩色 Scala 漂亮打印器(Li Haoyi)—— `pprintln(value)` 对 case class、集合与嵌套结构输出缩进的 fansi 着色结果。Ammonite REPL 把它作为默认的 `println` 替换。当输出不是 TTY 时通过 `pprint.PPrinter.BlackWhite` 切换调色板(或用 `pprint.copy(defaultWidth = 120, defaultIndent = 4)` 自定义布局)。

  • JLine 3

    JVM 终端抽象 —— Scala REPL 本身使用的库。`TerminalBuilder` 构造能力感知的 ANSI 终端(原始模式、信号处理、Windows VT 启用);`LineReader` 提供 readline 等价的行编辑(历史、补全、多行输入)。跨平台 —— Linux/macOS 通过 JNI 调 termios,Windows 通过 ConPTY。

常用写法

直接 println 配合 \u001B 与 Console.RED 标准库常量
// Scala 3 dropped octal escapes (\033) from string literals;
// \u001B (Unicode) is the only spelling that compiles on both
// Scala 2 and Scala 3. \e and \x1b are NOT supported in Scala
// string literals — the same constraint as Java and Kotlin.

object Main extends App {
  println("\u001B[1;31merror:\u001B[0m permission denied")
  println("\u001B[33mwarn:\u001B[0m deprecated flag")
  println("\u001B[32mok:\u001B[0m 142 tests passed")

  // Truecolor — 38;2;R;G;B
  println("\u001B[38;2;255;128;0morange truecolor\u001B[0m")

  // scala.io.AnsiColor — stdlib constants, no extra dep.
  // Console mixes in AnsiColor, so the constants are directly
  // accessible as Console.RED / GREEN / YELLOW / BLUE /
  // MAGENTA / CYAN / WHITE plus _B variants for backgrounds,
  // plus BOLD / UNDERLINED / BLINK / REVERSED / RESET.
  println(Console.RED + "error: " + Console.RESET + "permission denied")
  println(Console.BOLD + Console.GREEN + "ok" + Console.RESET +
          " 142 tests passed")
  println(Console.YELLOW_B + Console.BLACK + " CAUTION " +
          Console.RESET + " brakes wet")
}
fansi —— 可组合 fansi.Str、overlay、256 色板 + truecolor
// build.sbt:
//   libraryDependencies += "com.lihaoyi" %% "fansi" % "0.5.0"
//
// scala-cli:
//   //> using lib "com.lihaoyi::fansi:0.5.0"

import fansi.{Str, Color, Back, Bold, Underlined, Reversed}

// A fansi.Str carries structured colour spans, NOT raw bytes.
// .render materialises to the actual ANSI byte string.
val err: Str = Color.Red("error: ") ++ Str("permission denied")
println(err.render)

// Compose attributes via ++  (concatenate) or by stacking
// constructors:
val heading: Str =
  Color.Blue(Bold.On(Underlined.On("Section 1")))
println(heading.render)

// .overlay layers an attribute on a sub-range — bold the
// first 5 chars only:
val highlighted: Str =
  Str("FATAL server crashed").overlay(Bold.On, 0, 5)
println(highlighted.render)

// 256-palette via Color.Full / Back.Full (integer 0-255):
println(Color.Full(208)("256-palette orange").render)

// Truecolor — Color.True(r, g, b):
println(Color.True(255, 128, 0)("truecolor orange").render)

// Canonical Scala log scrubber — fansi.Str.ansiRegex is the
// right answer; rolling your own regex misses 256 / truecolor
// SGR forms and OSC sequences:
val dirty = "\u001B[1;31mERROR\u001B[0m at line 42"
val clean = fansi.Str.ansiRegex.replaceAllIn(dirty, "")
println(clean)   // → "ERROR at line 42"
能力门控 —— System.console + NO_COLOR + SBT 注意事项
// JVM-portable capability check:
//   - System.console() returns a Console when stdin AND stdout
//     are connected to a real terminal.
//   - Returns null when stdin/stdout is redirected to a file
//     or pipe (so 'scala foo.scala > out.log' disables colour).
//
// Pair with NO_COLOR env-var convention (https://no-color.org).
//
// SBT CAVEAT: SBT wraps the build's stdout to inject its own
// progress UI. Child-process ANSI output from `sbt run`
// sometimes loses colour. Fixes:
//   1) sbt -Dsbt.color=always run
//   2) In build.sbt:
//        ThisBuild / outputStrategy := Some(StdoutOutput)
//   3) Inside the sbt shell:
//        set ThisBuild / outputStrategy := Some(StdoutOutput)
//        reload
//        run
// scala-cli and mill do NOT share this quirk — they pass
// stdout through unchanged.

object AnsiCapable {
  def isCapable: Boolean = {
    if (sys.env.contains("NO_COLOR"))    false
    else if (sys.env.contains("FORCE_COLOR")) true
    else System.console() != null
  }

  def styled(text: String, sgr: String): String =
    if (isCapable) s"\u001B[${sgr}m${text}\u001B[0m" else text
}

@main def demo(): Unit = {
  println(AnsiCapable.styled("OK",   "32"))
  println(AnsiCapable.styled("FAIL", "1;31"))
}
pprint + JLine 3 —— 彩色漂亮打印 + 交互式行读取
// build.sbt:
//   libraryDependencies ++= Seq(
//     "com.lihaoyi" %% "pprint" % "0.9.0",
//     "org.jline"   %  "jline"  % "3.27.1",
//   )

// --- pprint: coloured pretty-printer (the Ammonite default) ---
import pprint.{pprintln, PPrinter}

case class User(name: String, email: String, age: Int)

// Default printer uses fansi colour for keys, types, strings.
pprintln(User("alice", "[email protected]", 30))
pprintln(List(1, 2, 3, 4, 5).map(x => x * x))

// Switch to the no-colour printer when output isn't a TTY:
val pp: PPrinter =
  if (System.console() != null) PPrinter.Color
  else                          PPrinter.BlackWhite
pp.pprintln(User("bob", "[email protected]", 25))

// --- JLine 3: interactive line editor with history + colour ---
import org.jline.terminal.TerminalBuilder
import org.jline.reader.LineReaderBuilder

val terminal = TerminalBuilder.builder()
  .system(true)
  .build()

val reader = LineReaderBuilder.builder()
  .terminal(terminal)
  .build()

// readLine accepts a prompt that may contain ANSI escapes:
val line = reader.readLine("\u001B[32m> \u001B[0m")
println(s"got: $line")

// JLine handles raw-mode termios on Linux/macOS and ConPTY on
// Windows automatically, so the prompt's ANSI bytes render
// correctly on every platform Scala's own REPL supports.

相关序列

其他语言