Skip to content

Commit f28e15c

Browse files
authored
Support :lines in System.cmd/3 (#12323)
1 parent 0a7881f commit f28e15c

File tree

2 files changed

+76
-23
lines changed

2 files changed

+76
-23
lines changed

lib/elixir/lib/system.ex

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -984,23 +984,38 @@ defmodule System do
984984
hello
985985
{%IO.Stream{}, 0}
986986
987+
If you want to read lines:
988+
989+
iex> System.cmd("echo", ["hello\nworld"], into: [], lines: 1024)
990+
{["hello", "world"], 0}
991+
987992
## Options
988993
989994
* `:into` - injects the result into the given collectable, defaults to `""`
995+
996+
* `:lines` - (since v1.15.0) reads the output by lines instead of in bytes. It expects a
997+
number of maximum bytes to buffer internally (1024 is a reasonable default).
998+
The collectable will be called with each finished line (regardless of buffer
999+
size) and without the EOL character
1000+
9901001
* `:cd` - the directory to run the command in
1002+
9911003
* `:env` - an enumerable of tuples containing environment key-value as
9921004
binary. The child process inherits all environment variables from its
9931005
parent process, the Elixir application, except those overwritten or
9941006
cleared using this option. Specify a value of `nil` to clear (unset) an
9951007
environment variable, which is useful for preventing credentials passed
996-
to the application from leaking into child processes.
1008+
to the application from leaking into child processes
1009+
9971010
* `:arg0` - sets the command arg0
1011+
9981012
* `:stderr_to_stdout` - redirects stderr to stdout when `true`
1013+
9991014
* `:parallelism` - when `true`, the VM will schedule port tasks to improve
10001015
parallelism in the system. If set to `false`, the VM will try to perform
10011016
commands immediately, improving latency at the expense of parallelism.
10021017
The default can be set on system startup by passing the "+spp" argument
1003-
to `--erl`.
1018+
to `--erl`
10041019
10051020
## Error reasons
10061021
@@ -1054,11 +1069,16 @@ defmodule System do
10541069
end
10551070

10561071
defp do_cmd(port_init, base_opts, opts) do
1057-
{into, opts} = cmd_opts(opts, [:use_stdio, :exit_status, :binary, :hide] ++ base_opts, "")
1072+
{into, line, opts} =
1073+
cmd_opts(opts, [:use_stdio, :exit_status, :binary, :hide] ++ base_opts, "", false)
1074+
10581075
{initial, fun} = Collectable.into(into)
10591076

10601077
try do
1061-
do_port(Port.open(port_init, opts), initial, fun)
1078+
case line do
1079+
true -> do_port_line(Port.open(port_init, opts), initial, fun, [])
1080+
false -> do_port_byte(Port.open(port_init, opts), initial, fun)
1081+
end
10621082
catch
10631083
kind, reason ->
10641084
fun.(initial, :halt)
@@ -1068,42 +1088,67 @@ defmodule System do
10681088
end
10691089
end
10701090

1071-
defp do_port(port, acc, fun) do
1091+
defp do_port_byte(port, acc, fun) do
10721092
receive do
10731093
{^port, {:data, data}} ->
1074-
do_port(port, fun.(acc, {:cont, data}), fun)
1094+
do_port_byte(port, fun.(acc, {:cont, data}), fun)
10751095

10761096
{^port, {:exit_status, status}} ->
10771097
{acc, status}
10781098
end
10791099
end
10801100

1081-
defp cmd_opts([{:into, any} | t], opts, _into),
1082-
do: cmd_opts(t, opts, any)
1101+
defp do_port_line(port, acc, fun, buffer) do
1102+
receive do
1103+
{^port, {:data, {:noeol, data}}} ->
1104+
do_port_line(port, acc, fun, [data | buffer])
1105+
1106+
{^port, {:data, {:eol, data}}} ->
1107+
data = [data | buffer] |> Enum.reverse() |> IO.iodata_to_binary()
1108+
do_port_line(port, fun.(acc, {:cont, data}), fun, [])
1109+
1110+
{^port, {:exit_status, status}} ->
1111+
# Data may arrive after exit status on line mode
1112+
receive do
1113+
{^port, {:data, {_, data}}} ->
1114+
data = [data | buffer] |> Enum.reverse() |> IO.iodata_to_binary()
1115+
{fun.(acc, {:cont, data}), status}
1116+
after
1117+
0 -> {acc, status}
1118+
end
1119+
end
1120+
end
1121+
1122+
defp cmd_opts([{:into, any} | t], opts, _into, line),
1123+
do: cmd_opts(t, opts, any, line)
1124+
1125+
defp cmd_opts([{:cd, bin} | t], opts, into, line) when is_binary(bin),
1126+
do: cmd_opts(t, [{:cd, bin} | opts], into, line)
10831127

1084-
defp cmd_opts([{:cd, bin} | t], opts, into) when is_binary(bin),
1085-
do: cmd_opts(t, [{:cd, bin} | opts], into)
1128+
defp cmd_opts([{:arg0, bin} | t], opts, into, line) when is_binary(bin),
1129+
do: cmd_opts(t, [{:arg0, bin} | opts], into, line)
10861130

1087-
defp cmd_opts([{:arg0, bin} | t], opts, into) when is_binary(bin),
1088-
do: cmd_opts(t, [{:arg0, bin} | opts], into)
1131+
defp cmd_opts([{:stderr_to_stdout, true} | t], opts, into, line),
1132+
do: cmd_opts(t, [:stderr_to_stdout | opts], into, line)
10891133

1090-
defp cmd_opts([{:stderr_to_stdout, true} | t], opts, into),
1091-
do: cmd_opts(t, [:stderr_to_stdout | opts], into)
1134+
defp cmd_opts([{:stderr_to_stdout, false} | t], opts, into, line),
1135+
do: cmd_opts(t, opts, into, line)
10921136

1093-
defp cmd_opts([{:stderr_to_stdout, false} | t], opts, into),
1094-
do: cmd_opts(t, opts, into)
1137+
defp cmd_opts([{:parallelism, bool} | t], opts, into, line) when is_boolean(bool),
1138+
do: cmd_opts(t, [{:parallelism, bool} | opts], into, line)
10951139

1096-
defp cmd_opts([{:parallelism, bool} | t], opts, into) when is_boolean(bool),
1097-
do: cmd_opts(t, [{:parallelism, bool} | opts], into)
1140+
defp cmd_opts([{:env, enum} | t], opts, into, line),
1141+
do: cmd_opts(t, [{:env, validate_env(enum)} | opts], into, line)
10981142

1099-
defp cmd_opts([{:env, enum} | t], opts, into),
1100-
do: cmd_opts(t, [{:env, validate_env(enum)} | opts], into)
1143+
defp cmd_opts([{:lines, max_line_length} | t], opts, into, _line)
1144+
when is_integer(max_line_length) and max_line_length > 0,
1145+
do: cmd_opts(t, [{:line, max_line_length} | opts], into, true)
11011146

1102-
defp cmd_opts([{key, val} | _], _opts, _into),
1147+
defp cmd_opts([{key, val} | _], _opts, _into, _line),
11031148
do: raise(ArgumentError, "invalid option #{inspect(key)} with value #{inspect(val)}")
11041149

1105-
defp cmd_opts([], opts, into),
1106-
do: {into, opts}
1150+
defp cmd_opts([], opts, into, line),
1151+
do: {into, line, opts}
11071152

11081153
defp validate_env(enum) do
11091154
Enum.map(enum, fn

lib/elixir/test/elixir/system_test.exs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ defmodule SystemTest do
173173
assert {["hello\n"], 0} = System.cmd("echo", ["hello"], opts)
174174
end
175175

176+
test "cmd/3 by line" do
177+
assert {["hello", "world"], 0} =
178+
System.cmd("echo", ["hello\nworld"], into: [], lines: 1024)
179+
180+
assert {["hello", "world"], 0} =
181+
System.cmd("echo", ["-n", "hello\nworld"], into: [], lines: 3)
182+
end
183+
176184
@echo "echo-elixir-test"
177185
@tag :tmp_dir
178186
test "cmd/3 with absolute and relative paths", config do

0 commit comments

Comments
 (0)