From 4a0cf6cef1cb3515fcdce8a6932edc21c5ee5f4f Mon Sep 17 00:00:00 2001 From: Kevin Schweikert Date: Thu, 21 May 2026 11:14:30 +0200 Subject: [PATCH 1/2] Colorize CLI error banner when ANSI is enabled The script-crash path in Kernel.CLI.print_error/3 now renders the exception banner (e.g. "** (ArgumentError) ...") in red when IO.ANSI.enabled?/0 is true. The stacktrace stays plain. Coloring is opt-in via a new ansi? argument on Kernel.CLI.format_error/4 that defaults to false, so callers that build Mix.Task.Compiler.Diagnostic messages from the same helper keep producing plain text and do not leak escape codes into existing consumers (see #13142). Assisted-by: claude-code:claude-opus-4-7 --- lib/elixir/lib/kernel/cli.ex | 13 +++++++++---- lib/elixir/test/elixir/kernel/cli_test.exs | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/kernel/cli.ex b/lib/elixir/lib/kernel/cli.ex index 9a55f941977..392ad023be3 100644 --- a/lib/elixir/lib/kernel/cli.ex +++ b/lib/elixir/lib/kernel/cli.ex @@ -98,7 +98,7 @@ defmodule Kernel.CLI do @doc """ Shared helper for error formatting on CLI tools. """ - def format_error(kind, reason, stacktrace) do + def format_error(kind, reason, stacktrace, ansi? \\ false) do {blamed, stacktrace} = Exception.blame(kind, reason, stacktrace) iodata = @@ -106,10 +106,10 @@ defmodule Kernel.CLI do %FunctionClauseError{} -> formatted = Exception.format_banner(kind, reason, stacktrace) padded_blame = pad(FunctionClauseError.blame(blamed, &inspect/1, &blame_match/1)) - [formatted, padded_blame] + [banner_ansi(formatted, ansi?), padded_blame] _ -> - Exception.format_banner(kind, blamed, stacktrace) + Exception.format_banner(kind, blamed, stacktrace) |> banner_ansi(ansi?) end [iodata, ?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))] @@ -179,9 +179,14 @@ defmodule Kernel.CLI do ## Error handling defp print_error(kind, reason, stacktrace) do - IO.write(:stderr, format_error(kind, reason, stacktrace)) + IO.write(:stderr, format_error(kind, reason, stacktrace, IO.ANSI.enabled?())) end + defp banner_ansi(banner, true), + do: IO.iodata_to_binary(IO.ANSI.format([:red, banner], true)) + + defp banner_ansi(banner, false), do: banner + defp blame_match(%{match?: true, node: node}), do: blame_ansi(:normal, "+", node) defp blame_match(%{match?: false, node: node}), do: blame_ansi(:red, "-", node) diff --git a/lib/elixir/test/elixir/kernel/cli_test.exs b/lib/elixir/test/elixir/kernel/cli_test.exs index cd09d72ed94..2f9296840de 100644 --- a/lib/elixir/test/elixir/kernel/cli_test.exs +++ b/lib/elixir/test/elixir/kernel/cli_test.exs @@ -197,6 +197,27 @@ defmodule Kernel.CLI.ExecutableTest do assert elixir(fixture_path("at_exit.exs") |> to_charlist()) == "goodbye cruel world with status 1\n" end + + describe "format_error/4" do + test "colors the banner in red when ansi? is true" do + formatted = + Kernel.CLI.format_error(:error, %ArgumentError{message: "boom"}, [], true) + |> IO.iodata_to_binary() + + assert formatted =~ IO.ANSI.red() + assert formatted =~ IO.ANSI.reset() + assert formatted =~ "** (ArgumentError) boom" + end + + test "leaves output plain by default" do + formatted = + Kernel.CLI.format_error(:error, %ArgumentError{message: "boom"}, []) + |> IO.iodata_to_binary() + + refute formatted =~ "\e[" + assert formatted =~ "** (ArgumentError) boom" + end + end end defmodule Kernel.CLI.RPCTest do From 103a8291d86f755d54d42aec3489cf15ac5d3a58 Mon Sep 17 00:00:00 2001 From: Kevin Schweikert Date: Thu, 21 May 2026 13:27:49 +0200 Subject: [PATCH 2/2] refactor: split formatting in banner and rest --- lib/elixir/lib/kernel/cli.ex | 23 +++++++++++----------- lib/elixir/test/elixir/kernel/cli_test.exs | 21 -------------------- 2 files changed, 12 insertions(+), 32 deletions(-) diff --git a/lib/elixir/lib/kernel/cli.ex b/lib/elixir/lib/kernel/cli.ex index 392ad023be3..84c9cb76bc0 100644 --- a/lib/elixir/lib/kernel/cli.ex +++ b/lib/elixir/lib/kernel/cli.ex @@ -98,21 +98,26 @@ defmodule Kernel.CLI do @doc """ Shared helper for error formatting on CLI tools. """ - def format_error(kind, reason, stacktrace, ansi? \\ false) do + def format_error(kind, reason, stacktrace) do + {banner, rest} = format_error_parts(kind, reason, stacktrace) + [banner, rest] + end + + defp format_error_parts(kind, reason, stacktrace) do {blamed, stacktrace} = Exception.blame(kind, reason, stacktrace) - iodata = + banner = case blamed do %FunctionClauseError{} -> formatted = Exception.format_banner(kind, reason, stacktrace) padded_blame = pad(FunctionClauseError.blame(blamed, &inspect/1, &blame_match/1)) - [banner_ansi(formatted, ansi?), padded_blame] + [formatted, padded_blame] _ -> - Exception.format_banner(kind, blamed, stacktrace) |> banner_ansi(ansi?) + Exception.format_banner(kind, blamed, stacktrace) end - [iodata, ?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))] + {banner, [?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]} end @doc """ @@ -179,14 +184,10 @@ defmodule Kernel.CLI do ## Error handling defp print_error(kind, reason, stacktrace) do - IO.write(:stderr, format_error(kind, reason, stacktrace, IO.ANSI.enabled?())) + {banner, rest} = format_error_parts(kind, reason, stacktrace) + IO.write(:stderr, [IO.ANSI.format([:red, banner]), rest]) end - defp banner_ansi(banner, true), - do: IO.iodata_to_binary(IO.ANSI.format([:red, banner], true)) - - defp banner_ansi(banner, false), do: banner - defp blame_match(%{match?: true, node: node}), do: blame_ansi(:normal, "+", node) defp blame_match(%{match?: false, node: node}), do: blame_ansi(:red, "-", node) diff --git a/lib/elixir/test/elixir/kernel/cli_test.exs b/lib/elixir/test/elixir/kernel/cli_test.exs index 2f9296840de..cd09d72ed94 100644 --- a/lib/elixir/test/elixir/kernel/cli_test.exs +++ b/lib/elixir/test/elixir/kernel/cli_test.exs @@ -197,27 +197,6 @@ defmodule Kernel.CLI.ExecutableTest do assert elixir(fixture_path("at_exit.exs") |> to_charlist()) == "goodbye cruel world with status 1\n" end - - describe "format_error/4" do - test "colors the banner in red when ansi? is true" do - formatted = - Kernel.CLI.format_error(:error, %ArgumentError{message: "boom"}, [], true) - |> IO.iodata_to_binary() - - assert formatted =~ IO.ANSI.red() - assert formatted =~ IO.ANSI.reset() - assert formatted =~ "** (ArgumentError) boom" - end - - test "leaves output plain by default" do - formatted = - Kernel.CLI.format_error(:error, %ArgumentError{message: "boom"}, []) - |> IO.iodata_to_binary() - - refute formatted =~ "\e[" - assert formatted =~ "** (ArgumentError) boom" - end - end end defmodule Kernel.CLI.RPCTest do