Launch a system command with stdin/stdout access

I do like Elixir as a scripting language except the BEAM is notoriously bad at working with external commands, even worse when those require the terminal.

For starters, this works.

port = Port.open({:spawn, "vi"}, [:nouse_stdio, :exit_status])

receive do
  {^port, {:exit_status, exit_status}} -> IO.puts "vi exited with #{exit_status}"
end

But ports are really designed to work with external programs custom tailored for your Erlang app.

One assumption is Erlang must talk to your external process, so pipes are connected to the child’s standard input/output. :nouse_stdio means “use file descriptors 3 and 4 instead” so vi inherits the terminal as standard input/output and things work as expected. System.cmd—which is simply a convenience wrapper around ports—won’t work because it is hardcoded to :use_stdio.

Next, Port.open does not block like subprocess.run. To mimic that, you can ask for the :exit_status then block with receive.

Finally, child processes spawned by Erlang ports are detached from the controlling terminal. So programs that read from the terminal directly like sudo won’t work. There’s no general workaround, only application-specific ones.

As you can see, it’s an uphill battle but keep in mind that Python is first and foremost a scripting language. If you don’t need to call external programs, .exs files and escripts are a pleasant way to automate. Otherwise, libraries can help ease some hurdles but not entirely.