How can I un-export a variable, without losing its value?

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

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

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.