18

Let's say I exported a variable:

foo=bar
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?

2
  • 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. Commented Jan 1, 2016 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
    Commented Jan 1, 2016 at 21:34

3 Answers 3

9

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"
    fi
    shift; shift; shift
  done
}
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
esac
3
  • 1
    For those unfamiliar with ${var+foo}, it evaluates to foo if var is set, even if empty, and nothing otherwise.
    – muru
    Commented Jan 2, 2016 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
    Commented Jan 2, 2016 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. Commented Jan 2, 2016 at 7:36
9

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

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

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

unexport()
    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 
          esac
    do    eval  set '"$'"{$1+$1}"'" "$'"$1"'" "$'@\" ###  $1 = (  $1+ ? $1 : "" )
          eval  "${1:+unset $1;$1=\$2;} shift 3"     ### $$1 = ( $1:+ ? $2 : -- )
    done

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.

unexport()
        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
                esac
        do      eval   getopts :s: '"$1" -"${'"$1+s}-\$$1\""
                eval   unset  "$1;  ${OPTARG+$1=\${OPTARG}#-}"
                shift
        done

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.

7
  • 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
    Commented Jan 2, 2016 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
    Commented Jan 2, 2016 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
    Commented Jan 2, 2016 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) Commented Jan 2, 2016 at 15:03
  • 1
    @PSkocik - done.
    – mikeserv
    Commented Jan 2, 2016 at 19:49

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .