Unset readonly variable in bash

Actually, you can unset a readonly variable. but I must warn that this is a hacky method. Adding this answer, only as information, not as a recommendation. Use it at your own risk. Tested on ubuntu 13.04, bash 4.2.45.

This method involves knowing a bit of bash source code & it's inherited from this answer.

$ readonly PI=3.14
$ unset PI
-bash: unset: PI: cannot unset: readonly variable
$ cat << EOF| sudo gdb
attach $$
call unbind_variable("PI")
detach
EOF
$ echo $PI

$

A oneliner answer is to use the batch mode and other commandline flags, as provided in F. Hauri's answer:

$ sudo gdb -ex 'call unbind_variable("PI")' --pid=$$ --batch

sudo may or may not be needed based on your kernel's ptrace_scope settings. Check the comments on vip9937's answer for more details.


I tried the gdb hack above because I want to unset TMOUT (to disable auto-logout), but on the machine that has TMOUT set as read only, I'm not allowed to use sudo. But since I own the bash process, I don't need sudo. However, the syntax didn't quite work with the machine I'm on.

This did work, though (I put it in my .bashrc file):

# Disable the stupid auto-logout
unset TMOUT > /dev/null 2>&1
if [ $? -ne 0 ]; then
    gdb <<EOF > /dev/null 2>&1
 attach $$
 call unbind_variable("TMOUT")
 detach
 quit
EOF
fi

Shortly: inspired by anishsane's answer

Edit 2021-11-10: Add (int) to cast unbind_variable result.

But with simplier syntax:

$ gdb -ex 'call (int) unbind_variable("PI")' --pid=$$ --batch

With some improvement, as a function:

My destroy function:

Or How to play with variable meta data. Note usage of rare bashisms: local -n VARIABLE=$1 and ${VARIABLE@a}...

destroy () { 
    declare -p $1 &>/dev/null || return -1 # Return if variable not exist
    local -n variable=$1
    local reslne result flags=${variable@a}
    [ -z "$flags" ] || [ "${flags//*r*}" ] && { 
        unset $1    # Don't run gdb if variable is not readonly.
        return $?
    }
    while read -r resline; do
        [ "$resline" ] && [ -z "${resline%%\$1 = *}" ] &&
            result=${resline##*1 = }
    done < <(
        exec gdb 2>&1 -ex 'call (int) unbind_variable("'$1'")' --pid=$$ --batch
    )
    return $result
}

You could copy this to a bash source file called destroy.bash, for sample...

Explanation:

 1  destroy () { 
 2      local -n variable=$1
 3      declare -p $1 &>/dev/null || return -1 # Return if variable not exist
 4      local reslne result flags=${variable@a}
 5      [ -z "$flags" ] || [ "${flags//*r*}" ] && { 
 6          unset $1    # Don't run gdb if variable is not readonly.
 7          return $?
 8      }
 9      while read resline; do
10          [ "$resline" ] && [ -z "${resline%\$1 = *}" ] &&
11                result=${resline##*1 = }
12      done < <(
13          gdb 2>&1 -ex 'call (int) unbind_variable("'$1'")' --pid=$$ --batch
14      )
15      return $result
16  }
  • line 2 create a local reference to submited variable.
  • line 3 prevent running on non existant variable
  • line 4 store parameter's attributes (meta) into $flags.
  • lines 5 to 8 will run unset instead of gdb if readonly flag not present
  • lines 9 to 12 while read ... result= ... done get return code of call (int) unbind_variable() in gdb output
  • line 13 gdb syntax with use of --pid and --ex (see gdb --help).
  • line 15 return $result of unbind_variable() command.

In use:

$ . destroy.bash
  • 1st with any regular (read-write) variable:

    $ declare PI=$(bc -l <<<'4*a(1)')
    $ echo $PI
    3.14159265358979323844
    $ echo ${PI@a} # flags
    
    $ declare -p PI
    declare -- PI="3.14159265358979323844"
    $ destroy PI
    $ echo $?
    0
    $ declare -p PI
    bash: declare: PI: not found
    
  • 2nd with read only variable:

    $ declare -r PI=$(bc -l <<<'4*a(1)')
    $ declare -p PI
    declare -r PI="3.14159265358979323844"
    $ echo ${PI@a} # flags
    r
    $ unset PI
    bash: unset: PI: cannot unset: readonly variable
    
    $ destroy PI
    $ echo $?
    0
    $ declare -p PI
    bash: declare: PI: not found
    
  • 3rd with non existant variable:

    $ destroy PI
    $ echo $?
    255
    

Tags:

Bash

Unset