diff --git a/lib/elixir/lib/kernel/cli.ex b/lib/elixir/lib/kernel/cli.ex index 08e18c462eb..58056f2a812 100644 --- a/lib/elixir/lib/kernel/cli.ex +++ b/lib/elixir/lib/kernel/cli.ex @@ -185,7 +185,15 @@ defmodule Kernel.CLI do defp print_error(kind, reason, stacktrace) do {banner, rest} = format_error_parts(kind, reason, stacktrace) - IO.write(:stderr, [IO.ANSI.format([:red, banner]), rest]) + error = [IO.ANSI.format([:red, banner]), rest] + + # stderr may not be writable (a closed descriptor, or a pipe whose reader + # has exited): the error cannot be shown, but the exit status still applies + try do + IO.write(:stderr, error) + rescue + _ -> :ok + end end defp blame_match(%{match?: true, node: node}), do: blame_ansi(:normal, "+", node) diff --git a/lib/elixir/test/elixir/kernel/cli_test.exs b/lib/elixir/test/elixir/kernel/cli_test.exs index 0407f351c2e..f11fd17fb5a 100644 --- a/lib/elixir/test/elixir/kernel/cli_test.exs +++ b/lib/elixir/test/elixir/kernel/cli_test.exs @@ -184,6 +184,21 @@ defmodule Kernel.CLI.ExecutableTest do "false\n" end + @tag :unix + @tag :tmp_dir + test "exits with status 1 when the error cannot be written to stderr", + %{tmp_dir: tmp_dir, cli_extension: cli_extension} do + # stderr is a pipe whose reader has exited, so printing the error fails; + # the status must still be the error's, and no crash dump written + cmd = + ~c"cd #{tmp_dir} && (#{elixir_executable(cli_extension)} -e \"raise ArgumentError\" " ++ + ~c"2>&1 >/dev/null; echo $? > status) | true" + + :os.cmd(cmd) + assert File.read!(Path.join(tmp_dir, "status")) == "1\n" + refute File.exists?(Path.join(tmp_dir, "erl_crash.dump")) + end + stderr_test "blames exceptions", %{cli_extension: cli_extension} do error = elixir(~c"-e \"Access.fetch :foo, :bar\"", cli_extension) assert error =~ "** (FunctionClauseError) no function clause matching in Access.fetch/2"