Tuesday, June 5, 2012

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


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?


Source: Tips4all

9 comments:

  1. Your shell process has a copy of the parent's environment and no access 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.

    ReplyDelete
  2. 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 "test vt100". As mentioned in another answer, there is also similar functionality in the INN UseNet news server.

    ReplyDelete
  3. You should use modules, see http://modules.sourceforge.net/

    ReplyDelete
  4. In my .bash_profile I have :

    # No Proxy
    function noproxy
    {
    /usr/local/sbin/noproxy #turn off proxy server
    unset http_proxy HTTP_PROXY https_proxy HTTPs_PROXY
    }


    # Proxy
    function setproxy
    {
    sh /usr/local/sbin/proxyon #turn on proxy server
    http_proxy=http://127.0.0.1:8118/
    HTTP_PROXY=$http_proxy
    https_proxy=$http_proxy
    HTTPS_PROXY=$https_proxy
    export http_proxy https_proxy HTTP_PROXY HTTPS_PROXY
    }


    So when I want to disable the proxy,
    the function(s) run in the login shell and sets the variables
    as expected and wanted.

    ReplyDelete
  5. It's "kind of" possible through using gdb and setenv(3), although I have a hard time recommending actually doing this. (Additionally, i.e. the most recent ubuntu won't actually let you do this without telling the kernel to be more permissive about ptrace, and the same may go for other distros as well).

    $ cat setfoo
    #! /bin/bash

    gdb /proc/${PPID}/exe ${PPID} <<END >/dev/null
    call setenv("foo", "bar", 0)
    END
    $ echo $foo

    $ ./setfoo
    $ echo $foo
    bar

    ReplyDelete
  6. This works - it isn't what I'd use, but it 'works':

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


    If the script is called "teredo", then:

    % env | grep SHELL
    SHELL=/bin/csh
    % env | grep TEREDO
    % teredo
    % env | grep TEREDO
    TEREDO_WORMS=ukelele
    % exit # Leaves the shell that was run by 'teredo'
    % # Oh, drat!
    % env | grep TEREDO
    % exec teredo
    % env | grep TEREDO
    TEREDO_WORMS=ukelele
    % # exit # will log you out (or, at least, exits the shell)


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

    ReplyDelete
  7. You can invoke another one Bash with the different bash_profile.
    Also, you can create special bash_profile for using in multi-bashprofile environment.

    Remember that you can use functions inside of bashprofile, and that functions will be avialable globally.
    for example, "function user { export USER_NAME $1 }" can set variable in runtime, for example: user olegchir && env | grep olegchir

    ReplyDelete
  8. Technically, that is correct -- only 'eval' doesn't fork another shell. However, from the point of view of the application you're trying to run in the modified environment, the difference is nil: the child inherits the environment of its parent, so the (modified) environment is conveyed to all descending processes.

    Ipso facto, the changed environment variable 'sticks' -- as long as you are running under the parent program/shell.

    If it is absolutely necessary for the environment variable to remain after the parent (Perl or shell) has exited, it is necessary for the parent shell to do the heavy lifting. One method I've seen in the documentation is for the current script to spawn an executable file with the necessary 'export' language, and then trick the parent shell into executing it -- always being cognizant of the fact that you need to preface the command with 'source' if you're trying to leave a non-volatile version of the modified environment behind. A Kluge at best.

    The second method is to modify the script that initiates the shell environment (.bashrc or whatever) to contain the modified parameter. This can be dangerous -- if you hose up the initialization script it may make your shell unavailable the next time it tries to launch. There are plenty of tools for modifying the current shell; by affixing the necessary tweaks to the 'launcher' you effectively push those changes forward as well.
    Generally not a good idea; if you only need the environment changes for a particular application suite, you'll have to go back and return the shell launch script to its pristine state (using vi or whatever) afterwards.

    In short, there are no good (and easy) methods. Presumably this was made difficult to ensure the security of the system was not irrevocably compromised.

    ReplyDelete
  9. 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.

    ReplyDelete