Synchronous command execution with version manager support.
Provides a clean interface to run shell commands with validation, shell selection, automatic detection of configuration files, asdf/mise integration, and structured result parsing.
For parallel execution, use Arrea.Parallel or Arrea.Leader.
Shell resolution
The shell is resolved with the following priority (lowest to highest):
Config.get(:shell)— consumer project or runtime config$SHELLenvironment variable- Current user login shell (
/etc/passwd) :shelloption passed toexecute/2— highest priority"sh"as a last resort if none of the above is valid
Both names ("zsh") and paths ("/bin/zsh") are accepted.
Names are resolved to paths via System.find_executable/1.
If the configured shell does not exist, it falls back to the system
default ($SHELL or "sh").
Based on the detected shell, its configuration file is forced to load
(e.g. ~/.bashrc for bash, ~/.zshrc for zsh, ~/.config/fish/config.fish for fish).
Real timeout
The timeout is enforced during execution: if the command does not
finish within timeout ms, the execution process is cancelled. The
underlying OS process receives SIGKILL when the Erlang port is closed
as its owner dies.
asdf/mise version management
Runtime versions can be forced via two mechanisms:
asdf —
asdf_<language>option: emitsexport ASDF_<LANG>_VERSION=<version>before the command. Works with both asdf and mise.mise —
mise_<language>option: wraps the command withmise exec <language>@<version> -- <command>.
Examples
iex> Arrea.Command.execute("echo hello")
{:ok, %{stdout: "hello\n", exit_code: 0, duration_ms: 3}}
iex> Arrea.Command.execute("mix test", asdf_elixir: "1.18.0")
{:ok, %{stdout: "...", exit_code: 0, duration_ms: 1200}}
iex> Arrea.Command.execute("node -v", mise_node: "20.0.0")
{:ok, %{stdout: "v20.0.0\n", exit_code: 0, duration_ms: 80}}
iex> Arrea.Command.execute("sleep 60", timeout: 500)
{:error, :timeout}
Summary
Functions
Extrae los argumentos mise_<lang> de las opciones y los formatea
como "lang@version" para el comando mise exec.
Returns true if the given command exists in the system PATH.
Executes a command string synchronously with optional configuration.
Executes a command with a language version managed by ASDF.
Parses a raw result map into a structured form.
Resolves the shell to use, by priority (lowest to highest)
Resolves the path to the shell configuration file.
Resolves a shell name to its absolute path.
Returns the full path of a command if found in PATH, or nil.
Types
@type result() :: %{ stdout: String.t(), exit_code: non_neg_integer(), duration_ms: non_neg_integer() }
Functions
Extrae los argumentos mise_<lang> de las opciones y los formatea
como "lang@version" para el comando mise exec.
Returns true if the given command exists in the system PATH.
Thin wrapper around System.find_executable/1 exposed publicly so
consumers (Apero.Proc, Botica.Batteries.*) don't have to redefine it
and can centralise the lookup through Arrea for future observability.
Examples
iex> Arrea.Command.command_exists?("echo")
true
iex> Arrea.Command.command_exists?("definitely_not_a_real_binary_12345")
false
Executes a command string synchronously with optional configuration.
The command is validated before execution unless :validate is set to
false. Invalid or dangerous commands return {:error, reason}
without executing anything.
The timeout is real: if the command does not finish within the limit, the execution process is actively cancelled (not post-hoc).
Options
:timeout— Maximum execution time in ms (default:30_000):cd— Working directory (default: current directory):shell— Shell to use — takes highest priority over config and env:shell_config— Path to the shell configuration file to load (optional):env— Additional environment variables as a map (optional):quiet— If true, suppress stderr capture (default: false):validate— Run the safety validator before execution (default:true). Set tofalsefor trusted internal callers (e.g. Apero's OS info commands) that would otherwise pay the per-call validation cost or hit false positives.:asdf_elixir— Force Elixir version via asdf/miseasdf_<lang>— Force any language version via asdf/misemise_<lang>— Force version viamise exec
Returns
{:ok, result}— Map with:stdout,:exit_code,:duration_ms{:error, :timeout}— The command was cancelled for exceeding the timeout{:error, reason}— Validation or execution error
@spec execute_with_asdf(String.t(), atom(), String.t(), keyword()) :: {:ok, result()} | {:error, term()}
Executes a command with a language version managed by ASDF.
Convenience wrapper around execute/2 that prepends the asdf shim activation.
Examples
iex> Command.execute_with_asdf("mix test", :elixir, "1.18.0")
{:ok, %{stdout: "...", exit_code: 0, duration_ms: 1200}}
@spec parse_result(result()) :: {:ok, result()} | {:error, {:exit_code, non_neg_integer()}}
Parses a raw result map into a structured form.
Detects common error patterns and returns tagged results.
Resolves the shell to use, by priority (lowest to highest):
Config.get(:shell)(project config orConfig.set/2)$SHELLenvironment variable- User login shell in
/etc/passwd :shelloption passed inopts— highest priority"sh"as a fallback if none is valid
If the resolved shell is a name (e.g. "zsh"), it is looked up in PATH.
If not found, it falls back to the system default.
Resolves the path to the shell configuration file.
Returns the expanded path to the config file (e.g. ~/.zshrc for zsh)
or nil if the shell has no known config file.
Resolves a shell name to its absolute path.
If it is already a path (contains /), it is returned as-is if it exists.
If it is just a name, it is looked up in PATH via System.find_executable/1.
Returns nil if the executable cannot be found.
Returns the full path of a command if found in PATH, or nil.
Counterpart to the POSIX which(1) command. Wraps
System.find_executable/1 for consistency with command_exists?/1.
Examples
iex> Arrea.Command.which("echo")
"/usr/bin/echo"
iex> Arrea.Command.which("not_a_real_binary_12345")
nil