From 023509e2ea28dd1070077ec523a406f6a4b276cf Mon Sep 17 00:00:00 2001 From: Peter Rice Date: Tue, 7 Jan 2025 01:22:55 -0500 Subject: [PATCH] Add an option to set env vars in the new process --- CHANGELOG.md | 5 +++++ gleam.toml | 2 +- src/shellout.gleam | 18 +++++++++++++++- src/shellout_ffi.erl | 7 +++++-- src/shellout_ffi.mjs | 10 ++++++++- test/shellout_test.gleam | 45 +++++++++++++++++++++++++++++++++++++++- 6 files changed, 81 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f8bcd5..56a3b90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/gleam.toml b/gleam.toml index c785a75..b6af176 100644 --- a/gleam.toml +++ b/gleam.toml @@ -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" diff --git a/src/shellout.gleam b/src/shellout.gleam index 7e2afbb..cde4258 100644 --- a/src/shellout.gleam +++ b/src/shellout.gleam @@ -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 @@ -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") @@ -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 diff --git a/src/shellout_ffi.erl b/src/shellout_ffi.erl index e404412..410f443 100644 --- a/src/shellout_ffi.erl +++ b/src/shellout_ffi.erl @@ -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, _} -> @@ -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, diff --git a/src/shellout_ffi.mjs b/src/shellout_ffi.mjs index f0914d4..ebd83df 100644 --- a/src/shellout_ffi.mjs +++ b/src/shellout_ffi.mjs @@ -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), @@ -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) { @@ -85,6 +89,7 @@ export function os_command(command, args, dir, opts) { stdin, stdout, stderr, + env, }; try { result = new Deno.Command(command, spawnOpts).outputSync(); @@ -92,6 +97,9 @@ export function os_command(command, args, dir, opts) { 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) { diff --git a/test/shellout_test.gleam b/test/shellout_test.gleam index 9c1cfbb..00050c0 100644 --- a/test/shellout_test.gleam +++ b/test/shellout_test.gleam @@ -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() @@ -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,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,2,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