Can a shell script set environment variables of the calling shell?

Viewed 653772

I'm trying to write a shell script that, when run, will set some environment variables that will stay set in the caller's shell.

setenv FOO foo

in csh/tcsh, or

export FOO=foo

in sh/bash only set it during the script's execution.

I already know that

source myscript

will run the commands of the script rather than launching a new shell, and that can result in setting the "caller's" environment.

But here's the rub:

I want this script to be callable from either bash or csh. In other words, I want users of either shell to be able to run my script and have their shell's environment changed. So 'source' won't work for me, since a user running csh can't source a bash script, and a user running bash can't source a csh script.

Is there any reasonable solution that doesn't involve having to write and maintain TWO versions on the script?

21 Answers

Your shell process has a copy of the parent's environment and no access to the parent process's environment whatsoever. When your shell process terminates any changes you've made to its environment are lost. Sourcing a script file is the most commonly used method for configuring a shell environment, you may just want to bite the bullet and maintain one for each of the two flavors of shell.

You're not going to be able to modify the caller's shell because it's in a different process context. When child processes inherit your shell's variables, they're inheriting copies themselves.

One thing you can do is to write a script that emits the correct commands for tcsh or sh based how it's invoked. If you're script is "setit" then do:

ln -s setit setit-sh

and

ln -s setit setit-csh

Now either directly or in an alias, you do this from sh

eval `setit-sh`

or this from csh

eval `setit-csh`

setit uses $0 to determine its output style.

This is reminescent of how people use to get the TERM environment variable set.

The advantage here is that setit is just written in whichever shell you like as in:

#!/bin/bash
arg0=$0
arg0=${arg0##*/}
for nv in \
   NAME1=VALUE1 \
   NAME2=VALUE2
do
   if [ x$arg0 = xsetit-sh ]; then
      echo 'export '$nv' ;'
   elif [ x$arg0 = xsetit-csh ]; then
      echo 'setenv '${nv%%=*}' '${nv##*=}' ;'
   fi
done

with the symbolic links given above, and the eval of the backquoted expression, this has the desired result.

To simplify invocation for csh, tcsh, or similar shells:

alias dosetit 'eval `setit-csh`'

or for sh, bash, and the like:

alias dosetit='eval `setit-sh`'

One nice thing about this is that you only have to maintain the list in one place. In theory you could even stick the list in a file and put cat nvpairfilename between "in" and "do".

This is pretty much how login shell terminal settings used to be done: a script would output statments to be executed in the login shell. An alias would generally be used to make invocation simple, as in "tset vt100". As mentioned in another answer, there is also similar functionality in the INN UseNet news server.

This works — it isn't what I'd use, but it 'works'. Let's create a script teredo to set the environment variable TEREDO_WORMS:

#!/bin/ksh
export TEREDO_WORMS=ukelele
exec $SHELL -i

It will be interpreted by the Korn shell, exports the environment variable, and then replaces itself with a new interactive shell.

Before running this script, we have SHELL set in the environment to the C shell, and the environment variable TEREDO_WORMS is not set:

% env | grep SHELL
SHELL=/bin/csh
% env | grep TEREDO
%

When the script is run, you are in a new shell, another interactive C shell, but the environment variable is set:

% teredo
% env | grep TEREDO
TEREDO_WORMS=ukelele
%

When you exit from this shell, the original shell takes over:

% exit
% env | grep TEREDO
%

The environment variable is not set in the original shell's environment. If you use exec teredo to run the command, then the original interactive shell is replaced by the Korn shell that sets the environment, and then that in turn is replaced by a new interactive C shell:

% exec teredo
% env | grep TEREDO
TEREDO_WORMS=ukelele
%

If you type exit (or Control-D), then your shell exits, probably logging you out of that window, or taking you back to the previous level of shell from where the experiments started.

The same mechanism works for Bash or Korn shell. You may find that the prompt after the exit commands appears in funny places.


Note the discussion in the comments. This is not a solution I would recommend, but it does achieve the stated purpose of a single script to set the environment that works with all shells (that accept the -i option to make an interactive shell). You could also add "$@" after the option to relay any other arguments, which might then make the shell usable as a general 'set environment and execute command' tool. You might want to omit the -i if there are other arguments, leading to:

#!/bin/ksh
export TEREDO_WORMS=ukelele
exec $SHELL "${@-'-i'}"

The "${@-'-i'}" bit means 'if the argument list contains at least one argument, use the original argument list; otherwise, substitute -i for the non-existent arguments'.

It's not what I would call outstanding, but this also works if you need to call the script from the shell anyway. It's not a good solution, but for a single static environment variable, it works well enough.

1.) Create a script with a condition that exits either 0 (Successful) or 1 (Not successful)

if [[ $foo == "True" ]]; then
    exit 0
else
    exit 1

2.) Create an alias that is dependent on the exit code.

alias='myscript.sh && export MyVariable'

You call the alias, which calls the script, which evaluates the condition, which is required to exit zero via the '&&' in order to set the environment variable in the parent shell.

This is flotsam, but it can be useful in a pinch.

I don't see any answer documenting how to work around this problem with cooperating processes. A common pattern with things like ssh-agent is to have the child process print an expression which the parent can eval.

bash$ eval $(shh-agent)

For example, ssh-agent has options to select Csh or Bourne-compatible output syntax.

bash$ ssh-agent
SSH2_AUTH_SOCK=/tmp/ssh-era/ssh2-10690-agent; export SSH2_AUTH_SOCK;
SSH2_AGENT_PID=10691; export SSH2_AGENT_PID;
echo Agent pid 10691;

(This causes the agent to start running, but doesn't allow you to actually use it, unless you now copy-paste this output to your shell prompt.) Compare:

bash$ ssh-agent -c
setenv SSH2_AUTH_SOCK /tmp/ssh-era/ssh2-10751-agent;
setenv SSH2_AGENT_PID 10752;
echo Agent pid 10752;

(As you can see, csh and tcsh uses setenv to set varibles.)

Your own program can do this, too.

bash$ foo=$(makefoo)

Your makefoo script would simply calculate and print the value, and let the caller do whatever they want with it -- assigning it to a variable is a common use case, but probably not something you want to hard-code into the tool which produces the value.

Other than writings conditionals depending on what $SHELL/$TERM is set to, no. What's wrong with using Perl? It's pretty ubiquitous (I can't think of a single UNIX variant that doesn't have it), and it'll spare you the trouble.

Related