Exporting variables from scripts

17 Mar 2023 10:08

Often, when you’re doing something in an interactive shell, you’d like to set an environment variable from a script. But you can’t, because scripts run as their own process, which means that they can’t set environment variables in their parent (the shell). How do we get around that?

Motivation

The one that’s on my mind right now is that every company I’ve worked for recently has a script to log into their AWS cloud, which results in setting the AWS_SESSION_TOKEN, AWS_ACCESS_KEY_ID, etc., environment variables.

Another example is where a script starts a background process, but needs to record its PID in the environment, so that other programs can access it. ssh-agent is an example of this; it exports the SSH_AUTH_SOCK and SSH_AGENT_PID environment variables.

Yet another example is something like rvm or nvm. nvm, for example, manipulates $PATH to add or update an entry that points to $NVM_VERSION_DIR/bin.

Using eval

The way that ssh-agent does it is by requiring the user to run it using eval, as follows:

eval `ssh-agent`

This causes the shell to run the command in backticks – or $(...) if you’d prefer – and then to evaluate the output as if it was commands entered at the prompt.

Running ssh-agent without the eval gives you something like the following:

SSH_AUTH_SOCK=/var/some-gibberish/ssh-whatever/agent.92365; export SSH_AUTH_SOCK;
SSH_AGENT_PID=92365; export SSH_AGENT_PID;
echo Agent pid 92365;

…which, when the shell executes them, will set the two environment variables and output a message.

Another example of using eval is the way that, say, direnv hook zsh (or direnv hook bash) or kubectl completion zsh work: as with ssh-agent, they output shell commands that set up various functions, aliases, hooks and so on.

Functions

Functions can also set environment variables. For example:

% hello() { HELLO=$1 }
% hello joe
% echo $HELLO
joe
% hello robert
% echo $HELLO
robert
The names are from Erlang: The Movie, in case you were wondering.

Using source

To get our shell to load those functions, we should use the source command. Where eval runs the command and evaluates the output, source just evaluates the file directly.

This is the mechanism that, for example, nvm uses. You add the following to your profile:

[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

Note that this uses . as a shorthand for source, and it escapes it with \, which prevents it from being treated as an alias.

So we can create a script, for example:

# scripts/hello.sh
hello() { HELLO=$1 }

…and then source it in our .bashrc, for example:

[ -s "$HOME/scripts/hello.sh" && source "$HOME/scripts/hello.sh" ]

Detecting whether we’re being run directly

It can sometimes be useful to be able to tell the difference between being source-ed or eval-ed. For example, you might want the hello command to print help, or do something actually useful if it’s run directly.

For details, start with this Stack Overflow question: https://stackoverflow.com/questions/2683279/how-to-detect-if-a-script-is-being-sourced.