Rambo v0.2.1 Rambo View Source
Ever stumped trying to get output from a port?
Rambo has one mission. Start your command, pipe standard input, send EOF and return with output.
Usage
Rambo.run("echo")
{:ok, %Rambo{status: 0, out: "\n", err: ""}}
If the command fails,
Rambo.run("printf")
{:error, %Rambo{status: 1, out: "", err: "usage: printf format [arguments ...]\n"}}
Send standard input to your command.
Rambo.run("cat", in: "rambo")
{:ok, %Rambo{status: 0, out: "rambo", err: ""}}
Pass arguments as a string or list of iodata.
Rambo.run("ls", "-la")
Rambo.run("ls", ["-l", "-a"])
Chain commands together. If one of them fails, the rest won’t be executed and the failing result is returned.
Rambo.run("ls") |> Rambo.run("sort") |> Rambo.run("head")
Logging
By default, Rambo streams standard error to the console as your command runs so you can spot errors before the command finishes.
Change this behaviour with the :log
option.
Rambo.run("ls", log: :stderr) # default
Rambo.run("ls", log: :stdout) # stream stdout only
Rambo.run("ls", log: true) # stream both
Rambo.run("ls", log: false) # don’t log output
You can stream logs to any function. It receives {:stdout, binary}
and
{:stderr, binary}
tuples whenever output is produced.
Rambo.run("echo", log: &IO.inspect/1)
{:stdout, "\n"}
{:ok, %Rambo{status: 0, out: "\n", err: ""}}
Kill
If your command is stuck, kill your command from another process and Rambo returns with any gathered results so far.
task = Task.async(fn ->
Rambo.run("cat")
end)
Rambo.kill(task.pid)
Task.await(task)
{:killed, %Rambo{status: nil, out: "", err: ""}}
Why?
Erlang ports do not work with programs that expect EOF to produce output. The only way to close standard input is to close the port, which also closes standard output preventing results from coming back to your app. This gotcha is marked Won’t Fix.
Design
When Rambo is asked to run a command, it creates a port to a shim. Then the shim runs your command, closes standard input and waits for output. After your command exits, its output is returned to your app before the port is closed and the shim exits.
+-----------------+ stdin
| +------+------+ --> +---------+
| Erlang | Port | Shim | | Command |
| +------+------+ <-- +---------+
+-----------------+ stdout
If the Erlang node stops during the command, your command is killed and the shim exits to avoid creating orphans (process leak).
Rambo does not start a pool of processes nor support bidirectional communication with your commands. It is intentionally kept simple and lightweight to run transient jobs with minimal overhead, such as calling a Python or Node script to transform some data. For more complicated use cases, see other libraries below.
Caveats
You cannot call Rambo.run
from a GenServer because Rambo uses receive
, which
interferes with GenServer’s receive
loop. However, you can wrap the call in a
Task.
task = Task.async(fn ->
Rambo.run("thingamabob")
end)
Task.await(task)
Comparisons
Rambo is the lightest, easiest library to manage external commands. While small and focused, Rambo has some niceties not available elsewhere. You can chain commands and easily stream your command’s output to any function.
System.cmd
If you don’t need to pipe standard input to your external program, just use
System.cmd
.
Porcelain
Porcelain cannot send EOF to trigger output by default. The Goon driver must be installed separately to add this capability. Rambo ships with the required native binaries.
Goon is written in Go, a multithreaded runtime with a garbage collector. To be as lightweight as possible, Rambo’s shim is written in Rust. No garbage collector, no runtime overhead.
Most importantly, Porcelain currently leaks processes. Writing a new driver to replace Goon should fix it, but Porcelain appears to be abandoned so effort went into creating Rambo.
erlexec
erlexec is great if you want fine grain control over external programs.
Each external OS process is mirrored as an Erlang process, so you get asynchronous and bidirectional communication. You can kill your OS processes with any signal or monitor them for termination, among many powerful features.
Choose erlexec if you want a kitchen sink solution.
Installation
Add rambo
to your list of dependencies in mix.exs
:
def deps do
[
{:rambo, "~> 0.2"}
]
end
This package bundles macOS, Linux and Windows binaries (x86-64 architecture only). For other environments, install the Rust compiler or Rambo won’t compile.
To remove unused binaries, set :purge
to true
in your configuration.
config :rambo,
purge: true
Links
License
Rambo is released under MIT license.
Link to this section Summary
Functions
Stop by killing your command.
Runs command
.
Runs command
with arguments or options.
Runs command
with arguments and options.
Link to this section Types
Link to this section Functions
Stop by killing your command.
Pass the pid
of the process that called run/1
. That process will return
with {:killed, %Rambo{}}
with results accumulated thus far.
Example
iex> task = Task.async(fn ->
...> Rambo.run("cat")
...> end)
iex> Rambo.kill(task.pid)
iex> Task.await(task)
{:killed, %Rambo{status: nil}}
Runs command
.
Executes the command
and returns {:ok, %Rambo{}}
or {:error, reason}
.
reason
is a string if the child process failed to start, or a %Rambo{}
struct if the child process started successfully but exited with a non-zero
status.
Multiple calls can be chained together with the |>
pipe operator to
simulate Unix pipes.
Rambo.run("ls") |> Rambo.run("sort") |> Rambo.run("head")
If any command did not exit with 0
, the rest will not be executed and the
last executed result is returned in an :error
tuple.
See run/2
or run/3
to pass arguments or options.
Examples
iex> Rambo.run("echo")
{:ok, %Rambo{out: "\n", status: 0, err: ""}}
Runs command
with arguments or options.
Arguments can be a string or list of strings. See run/3
for options.
Examples
iex> Rambo.run("echo", "john")
{:ok, %Rambo{out: "john\n", status: 0}}
iex> Rambo.run("echo", ["-n", "john"])
{:ok, %Rambo{out: "john", status: 0}}
iex> Rambo.run("cat", in: "john")
{:ok, %Rambo{out: "john", status: 0}}
Runs command
with arguments and options.
Options
:in
- pipe as standard input:cd
- the directory to run the command in:env
- map or list of tuples containing environment key-value as strings:log
- stream standard output or standard error to console or a function. May be:stdout
,:stderr
,true
for both,false
for neither, or a function with one arity. If a function is given, it will be passed{:stdout, output}
or{:stderr, error}
tuples. Defaults to:stderr
.
Examples
iex> Rambo.run("/bin/sh", ["-c", "echo $JOHN"], env: %{"JOHN" => "rambo"})
{:ok, %Rambo{out: "rambo\n", status: 0}}
iex> Rambo.run("echo", "rambo", log: &IO.inspect/1)
{:ok, %Rambo{out: "rambo\n", status: 0}}