4

The command

export FOO

should, according to my understanding, create an environment variable FOO (with a blank value) even if this variable did not exist previously and no value was supplied. This position seems to be supported by the zsh manual. See the following from man zshbuiltins:

export [ name[=value] ... ]
    The specified names are marked for automatic export to the environment of subsequently executed commands.    Equivalent
    to typeset -gx.  If a parameter specified does not already exist, it is created in the global scope.

However, when I use C's getenv function, this environment variable is not registered. here is a simple example. Consider the following program:

 % cat foo.c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    const char* foo = getenv("FOO");
    if ( foo == NULL ) {
        printf("The environment variable 'FOO' does not exist!\n");
    } else {
        printf("%s\n", foo);
        return 0;
    }
}

Compile it:

 % gcc --version
gcc (GCC) 7.2.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

 % gcc foo.c -o foo

Consider the following three executions:

 % ./foo
The environment variable 'FOO' does not exist!
 % export FOO
 % ./foo
The environment variable 'FOO' does not exist!
 % export FOO=BAR
 % ./foo
BAR

What is wrong in the middle case? Shouldn't it display a blank line?

sasquires
  • 356
  • 3
  • 15
  • 1
    This indeed creates an environment variable with a blank value, but in this case the value is `NULL`. – desmond_jones Feb 22 '20 at 19:57
  • @desmond_jones no, it's not `NULL`. Environment variables cannot be `NULL`, that doesn't mean anything, `NULL` only has a meaning in the C language. The environment variable is simply not defined. The command `export FOO` is only useful if `FOO` is already defined. – Marco Bonelli Feb 22 '20 at 20:41
  • @Marco: The command `export FOO` is also useful if `FOO` might be defined in the future. Note that shells provide various ways for variables to be defined. For example, `read` defines variables which you might want to mark as exportable. In bash, so does `printf -v`. – rici Feb 23 '20 at 16:27
  • @desmond_jones If you are correct, then this may be useful within a shell, i.e., I can do `export FOO` and then `echo $+FOO` and see that the shell believes that `FOO` has been set. But the whole point of environment variables is that they are communicated to child processes. What has been demonstrated here is that, from the point of view of a subprocess, it is impossible to distinguish between the case where `export FOO` has been called and the case where it hasn't. And this, to me, means that there has been no environment variable set. – sasquires Feb 23 '20 at 23:14
  • @desmond_jones I also think that Eric's point, which is that calling `export FOO` twice creates different behavior than calling it once, shows that there is something seriously wrong here. – sasquires Feb 23 '20 at 23:16

2 Answers2

3

Note that you didn't need to write your own program to display the env vars. The env command already exists :-)

This is a quirk that dates back at least four decades to the earliest UNIX shells. Modern shells (including bash, zsh, and ksh) all exhibit that quirk. The quirk is that if you export VAR and VAR has not already been assigned a value it is marked to be exported but will not actually be in the exported environment until you assign it a value. You can see this for yourself using any of those shells; not just zsh:

$ export FOO
$ env | grep FOO
$ FOO=''
$ env | grep FOO
FOO=
$ FOO=bar
$ env | grep FOO
FOO=bar
$ BAR=''
$ export BAR
$ env | grep BAR
BAR=

That last example hints at why export behaves this way.

Kurtis Rader
  • 6,734
  • 13
  • 20
  • Somehow I had assumed that `env` was a shell builtin and wouldn't tell me anything about how subprocesses actually see the environment. Thanks for pointing this out. – sasquires Feb 23 '20 at 04:04
  • There is still something bizarre about this, since the variable is obviously being created *and* marked for export. But I guess that unix history is full of such quirks. – sasquires Feb 23 '20 at 04:06
  • This is one case where `csh` actually behaves better than other shells! I just tried it and the behavior is correct there. – sasquires Feb 23 '20 at 04:07
  • This does not explain the observed behavior, notably that after `export FOO`, `zsh` does not export `FOO`, but after `export FOO; export FOO`, it does. `bash` does not behave this way. – Eric Postpischil Feb 23 '20 at 13:35
  • 1
    @sasquires: In addition to Eric's point about zsh, which I find compelling, you are making an assumption about what is "correct". Personally, I find the bash behaviour "correct", since it lets me mark variables as exportable independent of them being set. That makes it possible for the child process to distinguish between an environment variable which was not set and one which was explicitly set to the empty string. For variables for which the empty string is a valid value but not the desired default value, that distinction is important. – rici Feb 23 '20 at 16:00
  • Also, there's a good argument that the bash behaviour is more conformant with the Posix standard than what you might consider "correct". – rici Feb 23 '20 at 16:01
  • @rici I am not really convinced yet. This is not because of any intuitive notion of what is correct but because of this line from the `zsh` manual: "If a parameter specified does not already exist, it is created in the global scope." Both my test and Kurtis's test seem to indicate that this is not true, or else why would `getenv` return a `NULL` value and `env` be unaware of the variable's existence? – sasquires Feb 23 '20 at 23:12
  • @sasquires: that statement is incorrect or misleading. But it probably is also a clue as to the origin of the bug identified by Eric. There is a difference between unset variables and variables set to an empty value, but not all shells handle this difference the same way and there are probably people (csh users, for example) who will even disagree with my statement here. All I can say is that I find the difference useful, and that I believe Posix inclines that way too. – rici Feb 23 '20 at 23:21
  • @EricPostpischil I agree that there is something strange about the `zsh` behavior that probably represents a bug, as you correctly showed. But in my mind the `bash` behavior is also "wrong" in the sense that `export FOO` doesn't do something which is detectable from a subprocess. (Note that I haven't checked the `bash` manual to see if it is actually "wrong" from a technical perspective.) This is the point that Kurtis addressed. – sasquires Feb 23 '20 at 23:21
  • Most shells don't have scoped names. But many languages do, and most those languages distinguish between declaring that the name of a variable has a particular scope, and actually defining a value for that name. (Even there, there's lots of room for disagreement about details.) For me, placing a variable in the global scope and giving it a value are orthogonal, which is how bash works. But I don't mean to impose my views here. – rici Feb 23 '20 at 23:26
  • @rici Sure, I agree that it is nice to have a distinction between "set" and "set explicitly to a blank value." My question is: Why can't I distinguish these two cases in a subprocess? I guess that this ultimately boils down to how they are stored by the system in practice, which is as an array of `char*` or something like that. We would need to store more information to make the distinction above. This will give me away being more of a C++ programmer, but one obvious way would be to store a `std::pair< bool, char* >` instead of a `char*`; the first part indicates whether the variable was set. – sasquires Feb 23 '20 at 23:32
  • @sasquires: But you can distinguish the two cases. If `getenv("FOO")` returns a string, that's the value of the environment variable. If it returns `NULL`, then no value was exported. It's true that you can't distinguish between "no value was exported" and "`NO_VALUE` was exported", but my feeling is that the difference is not very useful, particularly if the main use case of export no value (or not exporting a value) is to leave the default setting in place. – rici Feb 24 '20 at 00:18
  • By the way, as you probably know, `env` is a NULL-terminated list of strings of the form `name=value`. It would be possible to express the idea that `FOO` was exported as an undefined value by placing `"FOO"` as a complete entry in `env`. Posix doesn't specify what happens if you do that, so I guess it's an available option for a shell which wanted to implement that feature. (Alternatively, it could put some non-name character before the `=`, which Posix doesn't prohibit. It only says that it impacts portability.) – rici Feb 24 '20 at 00:39
  • @rici That's an interesting idea. I suspect that it will never change just because of backwards-compatibility issues, but that would certainly be a great way to solve the problem. – sasquires Feb 24 '20 at 05:39
  • 1
    1/2: Keep in mind that the environment is *not* a set of variable values; it's just a set of strings that contain a `=`. *Conventionally*, most shells interpret those strings by splitting the string on the first `=`; if the first half is a valid identifier, a new shell variable is created by that name with the second half as the value. Other languages (Perl, for example) expose the environment as an associative array, with the first half as the key and the second half as the value. – chepner Feb 24 '20 at 14:04
  • 1
    2/2 There's simply no way to export *just* a name, with no value. You might think `name` (with no `=`) could do that, but as far as I can tell, POSIX doesn't provide a way to create such a string; references to the environment always refer to strings of the form `name=value`. – chepner Feb 24 '20 at 14:06
  • What @chepner said times 1000. You simply cannot change something like this after it has defacto behavior for four decades. – Kurtis Rader Feb 25 '20 at 03:31
2

There is some bug here. First, let’s change the program to use the environment variables directly, avoiding the possibility of a bug in getenv:

#include <stdio.h>
#include <string.h>


int main(int argc, char *argv[], char *arge[])
{
    for (char **p = arge; *p; ++p)
        if (0 == strncmp(*p, "FOO", 3))
        {
            puts(*p);
            return 0;
        }
}

Now, if we build this and execute it in a fresh zsh, we get no output, as expected.

If we export FOO and execute it, we again get no output, but export | grep FOO shows FOO=''. So zsh did define it, to be an empty string, but zsh failed to pass it to the program (or something in the environment-variable handling in the exec routines messed up).

However, start a fresh zsh, execute export FOO twice, and then the program. Now the output is FOO=. But export | grep FOO still shows FOO=''. So there seems to be some hidden state in zsh: Sometimes it does not export defined variables.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • `zsh` is definitely doing something weird. You can confirm this with `env` as well: `export FOO; env | grep FOO` outputs nothing, but running it a second time outputs `FOO=`. At least in `bash`, `export FOO` doesn't create a new variable; it just adds the export attribute to the name `FOO`. `[[ -v FOO ]]` has a non-zero exit status as a result. In `zsh`, `[[ -v FOO ]]` has 0 as its exit status, even after the first `export FOO` command. – chepner Feb 22 '20 at 22:55
  • @chepner. Yep. Also, `export -p` in bash distinguishes between "marked for export" and "marked for export with a value", as required by Posix (although it only uses Posix syntax if you start bash with the `--posix` flag). Zsh always shows the symbol as "marked for export with a value", even in cases where it doesn't actually export it. – rici Feb 22 '20 at 23:00
  • Thanks! At least you have confirmed that I'm not crazy. I'll leave this open for a little while to see if there are any `zsh` developers that want to comment on it, but otherwise I'll accept this answer. – sasquires Feb 23 '20 at 00:08
  • @rici Did you mean to link to https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#export? – chepner Feb 23 '20 at 13:14
  • @chepner: yes, specifically the `-p` option. Sorry. – rici Feb 23 '20 at 13:35