open_port's env option acts like an empty string as a value == an unset value

Forgive the use of Elixir in this post, but I think this ends up being specific to Erlang’s open_port

Short version, when using open_port and I set an environment variable to be an empty string, it’s not seen as different from an unset variable

Background: I was using System.shell to run a docker build command. My Dockerfile uses RUN test -n "${NODE_OPTIONS+set}" in order to make sure I’ve set that environment variable, however most of the time it’s empty. test -n cares only about the width of the text, so I use +set to populate it if it’s empty, but not unset, to help.

System.shell is using open_port, but adding options to the call, so I ran a test with Port.open, which is just a rename of :erlang.open_port. I expect the exit statuses below to be 1, 0, 0

iex(1)> port = Port.open({:spawn, ~c[test -n "${NODE_OPTIONS+set}"]}, [:exit_status, env: []])
#Port<0.3>
iex(2)> port = Port.open({:spawn, ~c[test -n "${NODE_OPTIONS+set}"]}, [:exit_status, env: [{~c"NODE_OPTIONS", ~c" "}]])
#Port<0.4>
iex(3)> port = Port.open({:spawn, ~c[test -n "${NODE_OPTIONS+set}"]}, [:exit_status, env: [{~c"NODE_OPTIONS", ~c""}]])
#Port<0.5>
iex(4)> flush()
{#Port<0.3>, {:exit_status, 1}}
{#Port<0.4>, {:exit_status, 0}}
{#Port<0.5>, {:exit_status, 1}}
:ok

Is this a bug, or have I missed something?

This is erlang 26.2.5.2

Welcome!

The code that handles that is here: otp/erts/emulator/beam/erl_bif_port.c at OTP-27.0 · erlang/otp · GitHub

There is a comment on top that says this:

/* Merges the global environment and the given {Key, Value} list into env,
 * unsetting all keys whose value is either 'false' or NIL. The behavior on
 * NIL is undocumented and perhaps surprising, but the previous implementation
 * worked in this manner. */

So the behaviour you are seeing has been like this for a long time and changing it will most likely break a bunch of systems so it will have to remain there. The documentation should IMO be updated to reflect this behaviour.

Thanks!

It took me a minute because of my expectation of :nil but I think this is telling me that NIL means an empty erlang string, aka [].

It looks like empty values could be added with an allow_nil_values option that would be passed into merge_global_environment. The is_nil(tuple[2]) check could be ignored if the above option is given, and then workflows that make use of empty shell variables can do so, as a shell environment would allow.

I must be a lightning rod for these things bc I remember bouncing off of something like this in vscode: Empty environment variables passed to vs code are unset · Issue #174330 · microsoft/vscode · GitHub (I’m not in that issue, it’s just one of the related issues to the same sort of issue)

I found the env tests. I cloned the repo in an attempt to run the tests and maybe add the new option, but make test in lib/std_lib complained about an error, and then hung. Ctrl-C’ing out then told me ‘The test(s) ran successfully (ct_run returned a success code)’. I’m already deep down a rabbit hole, and the number of new questions have multiplied further.

This is correct :smile: Though, I do believe 0x3b (what NIL is) is utilized outside the context of strings as well.

You may not have run ./configure && make && make release prior.