Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a command option to set environment variables #11

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## Unreleased

- The environment of launched processes can now be customized using the
`SetEnvironment` variant of `CommandOpt`.

## v1.6.0 - 2024-02-12

- Shellout now supports `gleam_stdlib` v1.0.
Expand Down
2 changes: 1 addition & 1 deletion gleam.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name = "shellout"
version = "1.6.0"
version = "1.7.0-dev"
description = "A Gleam library for cross-platform shell operations"
licences = ["Apache-2.0"]
gleam = ">= 0.34.0"
Expand Down
18 changes: 17 additions & 1 deletion src/shellout.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,14 @@ pub type CommandOpt {
/// input stream from behaving as usual.
///
OverlappedStdio
/// Set the given name-value pairs as environment variables in the spawned
/// process, replacing existing variables with the same names. A value of
/// `""` causes the variable to be unset.
///
/// If multiple `SetEnvironment` options are passed they will be combined,
/// with the last value for each name taking precedence.
///
SetEnvironment(List(#(String, String)))
}

/// Results in any output captured from the given `executable` on success, or an
Expand Down Expand Up @@ -410,10 +418,17 @@ pub fn command(
in directory: String,
opt options: List(CommandOpt),
) -> Result(String, #(Int, String)) {
let environment =
list.flat_map(options, fn(option) {
case option {
SetEnvironment(env) -> env
_ -> []
}
})
options
|> list.map(with: fn(opt) { #(opt, True) })
|> dict.from_list
|> do_command(executable, arguments, directory, _)
|> do_command(executable, arguments, directory, _, environment)
}

@external(erlang, "shellout_ffi", "os_command")
Expand All @@ -423,6 +438,7 @@ fn do_command(
arguments: List(String),
directory: String,
options: Dict(CommandOpt, Bool),
environment: List(#(String, String)),
) -> Result(String, #(Int, String))

/// Halts the runtime and passes the given `status` code to the operating
Expand Down
12 changes: 10 additions & 2 deletions src/shellout_ffi.erl
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
-module(shellout_ffi).

-export([os_command/4, os_exit/1, os_which/1, start_arguments/0]).
-export([os_command/5, os_exit/1, os_which/1, start_arguments/0]).

os_command(Command, Args, Dir, Opts) ->
os_command(Command, Args, Dir, Opts, EnvBin) ->
Which =
case os_which(Command) of
{error, _} ->
Expand All @@ -17,10 +17,18 @@ os_command(Command, Args, Dir, Opts) ->
{ok, Executable} ->
ExecutableChars = binary_to_list(Executable),
LetBeStdout = maps:get(let_be_stdout, Opts, false),
FromBin = fun({Name, Val}) ->
{
binary_to_list(Name),
unicode:characters_to_list(Val, file:native_name_encoding())
}
end,
Env = lists:map(FromBin, EnvBin),
PortSettings = lists:merge([
[
{args, Args},
{cd, Dir},
{env, Env},
eof,
exit_status,
hide,
Expand Down
10 changes: 9 additions & 1 deletion src/shellout_ffi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function start_arguments() {
return toList(globalThis.Deno?.args ?? process.argv.slice(1));
}

export function os_command(command, args, dir, opts) {
export function os_command(command, args, dir, opts, env_list) {
let executable = os_which(command);
executable = executable.isOk() ? executable : os_which(
path.join(dir, command),
Expand Down Expand Up @@ -76,6 +76,10 @@ export function os_command(command, args, dir, opts) {
process.on("SIGINT", () => Nil);
stdout = "inherit";
}
let env = {};
for (let e of env_list) {
env[e[0]] = e[1];
}

let result = {};
if (isDeno) {
Expand All @@ -85,13 +89,17 @@ export function os_command(command, args, dir, opts) {
stdin,
stdout,
stderr,
env,
};
try {
result = new Deno.Command(command, spawnOpts).outputSync();
} catch {}
result.status = result.code ?? null;
} else {
spawnOpts.stdio = [stdin, stdout, stderr];
if (env) {
spawnOpts.env = { ...process.env, ...env };
}
result = spawnSync(command, args, spawnOpts);
}
if (result.error) {
Expand Down
45 changes: 44 additions & 1 deletion test/shellout_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import gleam/dict
import gleam/string
import gleeunit
import gleeunit/should
import shellout.{type Lookups, LetBeStderr, LetBeStdout}
import shellout.{type Lookups, LetBeStderr, LetBeStdout, SetEnvironment}

pub fn main() {
gleeunit.main()
Expand Down Expand Up @@ -72,6 +72,49 @@ pub fn command_test() {
|> should.not_equal("")
}

pub fn environment_test() {
let print = fn(env, message) {
shellout.command(
run: "sh",
with: ["-c", string.concat(["echo -n ", message])],
in: ".",
opt: [SetEnvironment(env)],
)
}

print([#("TEST_1", "1"), #("TEST_2", "2")], "$TEST_1 $TEST_2 3")
|> should.equal(Ok("1 2 3"))

print([#("PATH", "/bin:/ビン")], "$PATH")
|> should.equal(Ok("/bin:/ビン"))

shellout.command(
run: "sh",
with: ["-c", string.concat(["echo -n $TEST_3 $TEST_2 $TEST_1"])],
in: ".",
opt: [
SetEnvironment([#("TEST_1", "3"), #("TEST_3", "3")]),
SetEnvironment([#("TEST_1", "1"), #("TEST_2", "2")]),
],
)
|> should.equal(Ok("3 2 1"))

let test_var = fn(env, var) {
shellout.command(
run: "sh",
with: ["-c", string.concat(["test -n \"$", var, "\""])],
in: ".",
opt: [SetEnvironment(env)],
)
}

test_var([], "HOME")
|> should.be_ok

test_var([#("HOME", "")], "HOME")
|> should.be_error
}

@target(erlang)
fn should_be_without_stdout(message) {
// Erlang ports can't separate stderr from stdout; it's all or nothing
Expand Down