9

I am interested in setting environmental variables of one shell instance from another. So I decided to do some research. After reading a number of questions about this I decided to test it out.

I spawned two shells A and B (PID 420), both running zsh. From shell A I ran the following.

sudo gdb -p 420
(gdb) call setenv("FOO", "bar", 1)
(gdb) detach

From shell B when I run env I can see the variable FOO is indeed set with a value of bar. This makes me think that FOO has been successfully initialised in the environment of shell B. However, if I try to print FOO I get an empty line implying it is not set. To me, it feels like there is a contradiction here.

This was tested on both my own Arch GNU/Linux system and an Ubuntu VM. I also tested this on bash where the variable didn't even show up in env. This although disappointing for me, makes sense if the shell caches a copy of its environment at spawn time and only uses that (which was suggested in one of the linked questions). This still doesn't answer why zsh can see the variable.

Why is the output of echo $FOO empty?


EDIT

After the input in the comments I decided to do a bit more testing. The results can be seen in the tables below. In the first column is the shell which the FOO variable was injected into. The first row contains the command whose output can be seen below it. The variable FOO was injected using: sudo gdb -p 420 -batch -ex 'call setenv("FOO", "bar", 1)'. The commands specific to zsh: zsh -c '...' were also tested using bash. The results were identical, their output was omitted for brevity.

Arch GNU/Linux, zsh 5.3.1, bash 4.4.12(1)

|      |  env | grep FOO  | echo $FOO |  zsh -c 'env | grep FOO'  |  zsh -c 'echo $FOO'  |         After export FOO          |
|------|------------------|-----------|---------------------------|----------------------|-----------------------------------|
| zsh  |  FOO=bar         |           | FOO=bar                   | bar                  | No Change                         |
| bash |                  | bar       |                           |                      | Value of FOO visible in all tests |

Ubuntu 16.04.2 LTS, zsh 5.1.1, bash 4.3.48(1)

|      |  env | grep FOO  | echo $FOO |  zsh -c 'env | grep FOO'  |  zsh -c 'echo $FOO'  |         After export FOO          |
|------|------------------|-----------|---------------------------|----------------------|-----------------------------------|
| zsh  |  FOO=bar         |           | FOO=bar                   | bar                  | No Change                         |
| bash |                  | bar       |                           |                      | Value of FOO visible in all tests |

The above seems to imply that the results are distribution agnostic. This doesn't tell me much more than zsh and bash handle setting of variables differently. Furthermore, export FOO has very different behaviour in this context depending on the shell. Hopefully these tests can make something clear to somebody else.

  • What happens, if you do a zsh -c 'echo $FOO' (use single quotes!) instead? Can you see it then? – user1934428 Jul 11 '17 at 4:45
  • The correct value is printed from a new sub shell (tested for bash child also). Clearly the environment is persistent somehow as the child can inherit it but why doesn't the parent honour it? – rlf Jul 11 '17 at 14:07
  • 3
    That's what I thought. I guess the shell has somewhere a symbol table of variables, some of them are marked as "exported", which means that upon opening a subshell, they are placed into the environment of the child process. Initially (when the shell starts), the variables from the environment at that time are copied into the symbol table (of course also as "exported" variables). When you change the environment, the shell doesn't get noticed to update their symbol table - but child processes (like env) see the modified environment. – user1934428 Jul 12 '17 at 6:09
  • 2
    I tested on Ubuntu 16.04 with zsh 5.1.1 and bash 4.3.48(1) and it seems setting an environment variable for zsh in GDB does not make it visible as a shell variable but does cause it to be passed on to child processes (as you have observed), while setting one for bash does make it visible as a shell variable but does not cause it to be passed on to child processes! It looks like zsh and bash use different strategies for managing variables, with zsh tracking non-environment variables and bash storing everything in its environment which it sanitizes when launching a (non-subshell) child. – Eliah Kagan Jul 19 '17 at 16:48
  • @EliahKagan, interesting; you should post that as an answer. I also wonder if it makes a difference if you run export FOO in bash? – Wildcard Jul 27 '17 at 23:48
2

Most shells don't use the getenv()/setenv()/putenv() API.

Upon start-up, they create shell variables for each environment variables. Those will be stored in internal structures that need to carry other information like whether the variable is exported, read-only... They can't use the libc's environ for that.

Similarly, and for that reason, they won't use execlp(), execvp() to execute commands but call the execve() system call directly, computing the envp[] array based on the list of their exported variables.

So in your gdb, you'd need to add an entry to that shells internal table of variables, or possibly call the right function that would make it interpret a export VAR=value code for it to update that table by itself.

As to why you see a difference between bash and zsh when you call setenv() in gdb, I suspect that's because you're calling setenv() before the shell initialises, for instance upon entering main().

You'll notice bash's main() is int main(int argc, char* argv[], char* envp[]) (and bash maps variables from those env vars in envp[]) while zsh's is int main(int argc, char* argv[]) and zsh gets the variables from environ instead. setenv() does modify environ but cannot modify envp[] in-place (read-only on several systems as well as the strings those pointers point to).

In any case, after the shell has read environ upon startup, using setenv() would be ineffective as the shell no longer uses environ (or getenv()) afterwards.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.