Launch a system command with stdin/stdout access

Hi everyone, this is my first post on this forum :slight_smile:
I am learning Elixir and I wish to write my software/scripts with it, I really love the syntax.
One problem I encountered is launching a system command and interact with it through stdin/stdout.
For example something like:

System.cmd("sudo", ["whoami"])
System.cmd("vi", [])

doesn’t work at all because they require access to stdin/stdout (seems like they can’t find a tty).
I searched a lot on the web and I found some resources with some workaround (eg. using a GUI askpass for sudo or launching system commands in another terminal); such resources I have found are a bit old.

My question is: is the situation changed? Is there any way, for example using Ports, to make the commands above work “as expected” like in other programming language?

For example in Python I write:

import subprocess
subprocess.run("vi")

This is the only “missing feature” that prevents me from porting all my software and scripts to Elixir.

Thanks in advance :slight_smile:

I know that sudo doesn’t even use stdin/stdout but reads from the (virtual) tty directly. I assume similar for vi.

To be honest, I wouldn’t even expect your python code to work…

And why port everything to Elixir? If you already have scripts in python that do their job, then keep them that way… I still prefer using zsh and python for doing scripts that I run from the terminal or via cron.

I use Elixir mainly for long running and daemon style applications.

2 Likes

Hi @zapateo, you may want to check out https://hexdocs.pm/porcelain/Porcelain.html, it has a spawn function that should be relatively easy to use for something like this.

I do agree with @NobbZ overall with respect to this idea though. Use the right tool for the right job. If you already have functioning python scripts for this stuff and there isn’t internal concurrency issues that a port to Elixir would provide clear wins with, I’d consider whether a port like this provides value. If you want to do it for educational reasons then that’s perfectly fine, but if you’re trying to solve problems, consider whether the problem is a good fit for what you have or a better fit with Elixir.

1 Like

+1 on porcelain, though check out https://github.com/jayjun/rambo as well, as I believe that supersedes porcelain… (I have only ever used porcelain few years back to do some pdf scraping…)

2 Likes

Thanks for your replies, guys! :slight_smile:

I tried both porcelain and rambo, they both look really cool for running system
processes/shell commands, but the problem still persist:

iex(1)> Porcelain.shell("vi")
Vim: Warning: Output is not to a terminal
Vim: Warning: Input is not from a terminal

Anyway, thanks for the feedback about choosing the right tool for the right job.
Porting my software in Elixir has started just for learning purposes but ended
in an overcomplicated mission.

1 Like

I’m not sure about the “not a terminal” problem, but usually if you want to continuously interact with an external program though stdin/stdout you should go for erlexec instead of rambo/porcelain.

1 Like

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.

2 Likes

Thanks a lot. Your answer clarified to me many aspects of the problem.

Also, the snippet you posted works as expected; so simple but very powerful, that it what I was looking for.

:slight_smile: