Current section
Files
Jump to
Current section
Files
src/shellout.gleam
//// A Gleam wrapper for [`Elixir.System.cmd/3`](https://hexdocs.pm/elixir/master/System.html#cmd/3)
////
import gleam/atom
import gleam/dynamic.{Dynamic, atom, int, list, string}
import gleam/function
import gleam/list
import gleam/result
import gleam/string
pub type CmdOpt {
Into(List(Nil))
Cd(String)
Env(List(tuple(String, String)))
Arg0(String)
StderrToStdout(Bool)
Parallelism(Bool)
}
pub type CmdResult =
Result(tuple(List(String), Int), String)
/// Executes the given `command` with `args`.
///
/// `command` is expected to be an executable available in PATH unless an
/// absolute path is given.
///
/// `args` must be a `List(String)` which the executable will receive as its
/// arguments as is. This means that:
/// - environment variables will not be interpolated
/// - wildcard expansion will not happen
/// - arguments do not need to be escaped or quoted for shell safety
///
/// This function returns a `Result`, where `success` is a
/// `tuple(List(String), Int)` containing a `List` of lines collected from
/// stdout, and the command exit status.
///
/// ## Examples
///
/// > shellout.cmd("printf", ["%s\n", "hi"], [])
/// Ok(tuple(["hi\n"], 0))
///
/// > let options = [Env([tuple("MIX_ENV", "test")])]
/// > shellout.cmd("printf", ["%s\n", "hi"], options)
/// Ok(tuple(["hi\n"], 0))
///
/// > shellout.cmd("", [], [StderrToStdout(True)])
/// Error("Error: Could not execute ``\n`` does not exist")
///
/// ## Options
///
/// - `Cd(String)` - the directory to run the command in
/// - `Env(List(tuple(String, String)))` - Tuples contain environment key-value
/// `String`s. The child process inherits all environment variables from its
/// parent process, the Gleam application, except those overwritten or cleared
/// using this option. Specify a value of `Nil` to clear (unset) an environment
/// variable, which is useful for preventing credentials passed to the
/// application from leaking into child processes.
/// - `Arg0(String)` - sets the command arg0
/// - `StderrToStdout(Bool)` - redirects stderr to stdout when `True`
/// - `Parallelism(Bool)` - when `True`, the VM will schedule port tasks to
/// improve parallelism in the system. If set to `False`, the VM will try to
/// perform commands immediately, improving latency at the expense of
/// parallelism. The default can be set on system startup by passing the "+spp"
/// argument to `--erl`.
///
/// *Documentation adapted from [`Elixir.System.cmd/3`](https://hexdocs.pm/elixir/master/System.html#cmd/3)*
///
pub fn cmd(
bin command: String,
args args: List(String),
opts opts: List(CmdOpt),
) -> CmdResult {
cmd_decoder(command, args, opts)
}
fn cmd_decoder(command, args, opts) -> CmdResult {
let opts =
opts
|> list.append([Into([])])
let default_error = tuple(
atom.create_from_string("error"),
atom.create_from_string("nil"),
)
fn() {
external_cmd(command, args, opts)
|> dynamic.typed_tuple2(
fn(list) {
list
|> dynamic.typed_list(string)
},
int,
)
}
|> function.rescue
|> result.map_error(fn(exception) {
let exception =
exception
|> dynamic.from
|> dynamic.typed_tuple2(atom, atom)
|> result.unwrap(or: default_error)
let tuple(_type, reason) = exception
let reason = case atom.to_string(reason) {
"argument_error" -> "Invalid arguments given"
"system_limit" -> "All Erlang emulator ports are in use"
"enomem" -> "Not enough memory"
"eagain" -> "No operating system processes available"
"enametoolong" -> "Command name is too long"
"emfile" -> "No available file descriptors"
"enfile" -> "File table is full"
"eacces" -> string.concat(["`", command, "` is not executable"])
"enoent" -> string.concat(["`", command, "` does not exist"])
_ -> "Unknown error"
}
string.concat(["Error: Could not execute `", command, "`\n", reason])
})
|> result.flatten
}
external fn external_cmd(String, List(String), List(CmdOpt)) -> Dynamic =
"Elixir.System" "cmd"