Let's say I exported a variable:

export foo

Now, I'd like to un-export it. That's to say, if I do sh -c 'echo "$foo"' I shouldn't get bar. foo shouldn't show up in sh -c's environment at all. sh -c is merely an example, an easy way to show the presence of a variable. The command could be anything - it may be something whose behaviour is affected simply by the presence of the variable in its environment.

I can:

  1. unset the variable, and lose it
  2. Remove it using env for each command: env -u foo sh -c 'echo "$foo"'
    • impractical if you want to continue using the current shell for a while.

Ideally, I'd want to keep the value of the variable, but not have it show up at all in a child process, not even as an empty variable.

I guess I could do:

otherfoo="$foo"; unset foo; foo="$otherfoo"; unset otherfoo

This risks stomping over otherfoo, if it already exists.

Is that the only way? Are there any standard ways?

  • 1
    You could echo the value into a temporary file, using mktemp if that is portable enough, and unset the value, and source the temporary file to assign the variable. At least a temporary file can be created with a more or less arbitrary name in contrast to a shell variable. – Thomas Dickey Jan 1 '16 at 21:02
  • @Sukminder The sh -c command is merely an example. Take any command within which you cannot unset a variable in its stead, if you will. – muru Jan 1 '16 at 21:34

There's no standard way.

You can avoid using a temporary variable by using a function. The following function takes care to keep unset variables unset and empty variables empty. It does not however support features found in some shells such as read-only or typed variables.

unexport () {
  while [ "$#" -ne 0 ]; do
    eval "set -- \"\${$1}\" \"\${$1+set}\" \"\$@\""
    if [ -n "$2" ]; then
      unset "$3"
      eval "$3=\$1"
    shift; shift; shift
unexport foo bar

In ksh, bash and zsh, you can unexport a variable with typeset +x foo. This preserves special properties such as types, so it's preferable to use it. I think that all shells that have a typeset builtin have typeset +x.

case $(LC_ALL=C type typeset 2>&1) in
  typeset\ *\ builtin) unexport () { typeset +x -- "$@"; };;
  *) unexport () { … };; # code above
  • 1
    For those unfamiliar with ${var+foo}, it evaluates to foo if var is set, even if empty, and nothing otherwise. – muru Jan 2 '16 at 0:31
  • Say, do you have any comments on typeset +x vs export -n for the shells that do support the former? Is export -n rarer, or does it not preserve some properties? – muru Jan 2 '16 at 3:44
  • @muru If you're writing a bash script, you can use export -n or typeset +x indifferently. In ksh or zsh, there's only typeset +x. – Gilles Jan 2 '16 at 7:36

EDIT: For bash only, as pointed out in the comments:

The -n option to export removes the export property from each given name. (See help export.)

So for bash, the command you want is: export -n foo

  • 1
    That's shell-specific (see POSIX), OP did not specify a shell, but asked for a standard way of solving the problem. – Thomas Dickey Jan 1 '16 at 21:31
  • @ThomasDickey, wasn't aware of that. Thanks, updated. – Wildcard Jan 1 '16 at 21:38

I wrote a similar POSIX function, but this doesn't risk arbitrary code execution:

    while case ${1##[0-9]*} in                   ### rule out leading numerics
          (*[!_[:alnum:]]*|"")                   ### filter out bad|empty names
          set "" ${1+"bad name: '$1'"}           ### prep bad name error
          return ${2+${1:?"$2"}}                 ### fail w/ above err or return 
    do    eval  set '"$'"{$1+$1}"'" "$'"$1"'" "$'@\" ###  $1 = (  $1+ ? $1 : "" )
          eval  "${1:+unset $1;$1=\$2;} shift 3"     ### $$1 = ( $1:+ ? $2 : -- )

It will also handle as many arguments as you care to provide it. If an argument is a valid name that is not otherwise already set it is silently ignored. If an argument is a bad name it writes to stderr and halts as appropriate, though any valid name preceding an invalid on its command-line will still be processed.

I thought of another way. I like it a lot better.

        while   unset OPTARG; OPTIND=1           ### always work w/ $1
                case  ${1##[0-9]*}    in         ### same old same old
                (*[!_[:alnum:]]*|"")             ### goodname && $# > 0 || break
                    ${1+"getopts"} : "$1"        ### $# ? getopts : ":"
                    return                       ### getopts errored or ":" didnt
        do      eval   getopts :s: '"$1" -"${'"$1+s}-\$$1\""
                eval   unset  "$1;  ${OPTARG+$1=\${OPTARG}#-}"

Well, both of these use a lot of the same techniques. Basically if a shell var is unset a reference to it will not expand with a + parameter expansion. But if it is set - regardless of its value - a parameter expansion like: ${parameter+word} will expand to word - and not to the variable's value. And so shell variables self-test and self-substitute on success.

They can also self-fail. In the top function if a bad name is found I move $1 into $2 and leave $1 null because the next thing I do is either return success if all args have been processed and the loop is at an end, or, if the arg was invalid, the shell will expand the $2 into $1:? which will kill a scripted shell and return an interrupt to an interactive one while writing word to stderr.

In the second one getopts does the assignments. And it won't assign a bad name - rather write it will write out a standard error message to stderr. What's more it saves the arg's value in $OPTARG if the argument was the name of a set variable in the first place. So after doing getopts all that is needed is to eval a set OPTARG's expansion into the appropriate assignment.

  • 2
    One of these days, I'll be in the psychiatric ward somewhere after trying to wrap my head around one of your answers. :D How does the other answer suffer from arbitrary code execution? Can you provide an example? – muru Jan 2 '16 at 4:11
  • 3
    @muru - not unless an argument is an invalid name. but that's not the problem - the problem is that input is not validated. yes, it is required that you pass it an argument with a weird name to make it execute arbitrary code - but that's pretty much the basis for every CVE in history. if you try to export a weird name, it won't kill your computer. – mikeserv Jan 2 '16 at 4:31
  • 1
    @muru - oh, and the arguments can be arbitrary: var=something; varname=var; export "$varname" is perfectly valid. the same goes for unset, and with this and with the other, but the minute the contents of that "$varname" variable get crazy, it could be regrettable. and that's pretty how that whole bash function export debacle happened anyway. – mikeserv Jan 2 '16 at 4:38
  • 1
    @mikeserv i think you'd get a lot more upvotes (at least from me) if you replaced that obfuscated code with code that explains itself (or commented the lines, at least) – PSkocik Jan 2 '16 at 15:03
  • 1
    @PSkocik - done. – mikeserv Jan 2 '16 at 19:49

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.