Skip to content

Add support for use_stdio option to System.shell and Mix.Shell.cmd #13580

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions lib/elixir/lib/system.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1053,7 +1053,11 @@ defmodule System do

* `:arg0` - sets the command arg0

* `:stderr_to_stdout` - redirects stderr to stdout when `true`
* `:stderr_to_stdout` - redirects stderr to stdout when `true`, no effect
if `use_stdio` is `false``.

* `:use_stdio` - `true` by default, setting it to false allows direct
interaction with the terminal from the callee

* `:parallelism` - when `true`, the VM will schedule port tasks to improve
parallelism in the system. If set to `false`, the VM will try to perform
Expand Down Expand Up @@ -1179,6 +1183,13 @@ defmodule System do
defp cmd_opts([{:stderr_to_stdout, false} | t], opts, into, line),
do: cmd_opts(t, opts, into, line)

defp cmd_opts([{:use_stdio, false} | t], opts, into, line),
do: cmd_opts(t, [:nouse_stdio | List.delete(opts, :use_stdio)], into, line)

# use_stdio is true by default, do nothing but match it
defp cmd_opts([{:use_stdio, true} | t], opts, into, line),
do: cmd_opts(t, opts, into, line)

defp cmd_opts([{:parallelism, bool} | t], opts, into, line) when is_boolean(bool),
do: cmd_opts(t, [{:parallelism, bool} | opts], into, line)

Expand All @@ -1192,8 +1203,12 @@ defmodule System do
defp cmd_opts([{key, val} | _], _opts, _into, _line),
do: raise(ArgumentError, "invalid option #{inspect(key)} with value #{inspect(val)}")

defp cmd_opts([], opts, into, line),
do: {into, line, opts}
defp cmd_opts([], opts, into, line) do
if :stderr_to_stdout in opts and :nouse_stdio in opts,
do: raise(ArgumentError, "cannot use `stderr_to_stdout: true` and `use_stdio: false`")

{into, line, opts}
end

defp validate_env(enum) do
Enum.map(enum, fn
Expand Down
42 changes: 38 additions & 4 deletions lib/elixir/test/elixir/system_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ defmodule SystemTest do
env: %{"foo" => "bar", "baz" => nil},
arg0: "echo",
stderr_to_stdout: true,
parallelism: true
parallelism: true,
use_stdio: true
]

assert {["hello\r\n"], 0} = System.cmd("cmd", ~w[/c echo hello], opts)
Expand Down Expand Up @@ -146,7 +147,8 @@ defmodule SystemTest do
cd: File.cwd!(),
env: %{"foo" => "bar", "baz" => nil},
stderr_to_stdout: true,
parallelism: true
parallelism: true,
use_stdio: true
]

assert {["bar\r\n"], 0} = System.shell("echo %foo%", opts)
Expand All @@ -167,12 +169,39 @@ defmodule SystemTest do
env: %{"foo" => "bar", "baz" => nil},
arg0: "echo",
stderr_to_stdout: true,
parallelism: true
parallelism: true,
use_stdio: true
]

assert {["hello\n"], 0} = System.cmd("echo", ["hello"], opts)
end

test "cmd/3 (can't use `use_stdio: false, stderr_to_stdout: true`)" do
opts = [
into: [],
cd: File.cwd!(),
env: %{"foo" => "bar", "baz" => nil},
arg0: "echo",
stderr_to_stdout: true,
use_stdio: false
]

message = ~r"cannot use `stderr_to_stdout: true` and `use_stdio: false`"

assert_raise ArgumentError, message, fn ->
System.cmd("echo", ["hello"], opts)
end
end

test "cmd/3 (`use_stdio: false`)" do
opts = [
into: [],
use_stdio: false
]

assert {[], 0} = System.cmd("echo", ["hello"], opts)
end

test "cmd/3 by line" do
assert {["hello", "world"], 0} =
System.cmd("echo", ["hello\nworld"], into: [], lines: 1024)
Expand Down Expand Up @@ -228,11 +257,16 @@ defmodule SystemTest do
into: [],
cd: File.cwd!(),
env: %{"foo" => "bar", "baz" => nil},
stderr_to_stdout: true
stderr_to_stdout: true,
use_stdio: true
]

assert {["bar\n"], 0} = System.shell("echo $foo", opts)
end

test "shell/2 (non-interactive)" do
assert {[], 0} = System.shell("echo hello", into: [], use_stdio: false)
end
end

@tag :unix
Expand Down
10 changes: 7 additions & 3 deletions lib/mix/lib/mix/shell.ex
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ defmodule Mix.Shell do

* `:cd` *(since v1.11.0)* - the directory to run the command in

* `:stderr_to_stdout` - redirects stderr to stdout, defaults to true
* `:stderr_to_stdout` - redirects stderr to stdout, defaults to true, unless use_stdio is set to false

* `:use_stdio` - controls whether the command should use stdin / stdout / stdrr, defaults to true

* `:env` - a list of environment variables, defaults to `[]`

Expand All @@ -119,11 +121,13 @@ defmodule Mix.Shell do
callback
end

use_stdio = Keyword.get(options, :use_stdio, true)

options =
options
|> Keyword.take([:cd, :stderr_to_stdout, :env])
|> Keyword.take([:cd, :stderr_to_stdout, :env, :use_stdio])
|> Keyword.put(:into, %Mix.Shell{callback: callback})
|> Keyword.put_new(:stderr_to_stdout, true)
|> Keyword.put_new(:stderr_to_stdout, use_stdio)

{_, status} = System.shell(command, options)
status
Expand Down