Skip to content

Commit

Permalink
Add an option to set env vars in the new process
Browse files Browse the repository at this point in the history
  • Loading branch information
pvsr committed Jan 14, 2025
1 parent 8b0cc44 commit cf6f224
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 6 deletions.
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
7 changes: 5 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,13 @@ 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), binary_to_list(Val)} 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:/bin2")], "$PATH")
|> should.equal(Ok("/bin:/bin2"))

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

0 comments on commit cf6f224

Please sign in to comment.