106

How do I unset a readonly variable in Bash?

$ readonly PI=3.14

$ unset PI
bash: PI: readonly variable

or is it not possible?

codeforester
  • 39,467
  • 16
  • 112
  • 140
Kokizzu
  • 24,974
  • 37
  • 137
  • 233
  • 1
    ah my bad http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_10_01.html Make variables read-only. These variables cannot then be assigned values by subsequent assignment statements, nor can they be unset. – Kokizzu Jul 01 '13 at 03:39
  • Usually the variables are read only because **/etc/profile** contains a lot of lines like this `readonly TMOUT`. I prefer to comment those lines and to open a new connection to that Linux machine. – ROMANIA_engineer Dec 08 '16 at 18:28
  • 1
    @ROMANIA_engineer Or, simply exec bash --norc, then set the stuff you do want manually, or in your own rc file - eg: source ~/.gnbashrc – Graham Nicholls Feb 06 '19 at 13:23
  • I wonder: *Why* do you want to unset the variable; to save RAM? In any case when unsetting the variable to indirectly change the value to the empty string (unless using `set -u`, too). – U. Windl Jan 09 '23 at 14:38
  • I already forgot why i need this '__') – Kokizzu Jan 12 '23 at 18:29

16 Answers16

130

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.

anishsane
  • 20,270
  • 5
  • 40
  • 73
  • 62
    Now that is what I would call redneck bash programming ;) – Floyd Dec 29 '13 at 08:12
  • 7
    Note: Don't get tempted to change `cat << EOF| sudo gdb` to `sudo gdb << EOF`. It _may_ **not** work, since the redirected input provider - `bash` is being stopped due to `gdb` attachment. – anishsane Aug 13 '14 at 06:46
  • I think [this](http://stackoverflow.com/questions/17397069/unset-readonly-variable-in-bash#21294582) response is a little more accurate, since it does not require sudo and is ending gdb properly – Rafareino Aug 19 '15 at 18:59
  • 1
    ^^ EOF at stdin & explicit quit would both quit gdb cleanly. – anishsane Aug 20 '15 at 06:30
  • @anishsane using `gdb` in script mode could be usefull if many `attach` have to be done. For uniq process, use `--pid` flag... and for single command, u could use `-ex`, see [my answer](https://stackoverflow.com/a/53458406/1765658): Something like `gdb -ex 'call unbind_variable("PI")' --pid=$$ --batch` could be enough. – F. Hauri - Give Up GitHub Nov 24 '18 at 13:13
  • 1
    ^^ Oh, yes, that's indeed handy. :-) – anishsane Nov 25 '18 at 04:11
  • 2
    I like one liners: `echo -e "attach $$\n call unbind_variable(\"PI\")\n detach" | gdb` – Satya Mishra May 01 '19 at 22:27
  • 1
    @SatyaMishra This could by written a lot simplier in one line!! See [my comment](https://stackoverflow.com/questions/17397069/unset-readonly-variable-in-bash/53458406#comment93788639_17398009) and [my answer](https://stackoverflow.com/a/53458406/1765658) – F. Hauri - Give Up GitHub Nov 10 '21 at 15:06
56

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
vip9937
  • 561
  • 4
  • 3
  • 3
    I would suggest using `-q -n` options to silence `gdb` an not load any _.gdbinit_ file per safety. – Lucas Cimon Nov 06 '14 at 17:55
  • 4
    *"since I own the bash process, I don't need sudo"* Note that this depends on what operating system you are using and how it is configured. [With most currently used versions of the Linux kernel](https://www.kernel.org/doc/Documentation/security/Yama.txt) this is [controlled through `/proc/sys/kernel/yama/ptrace_scope`](https://askubuntu.com/q/41629/22949). The most common values are `0`, in which case you can do this, and `1`, in which case [you probably can't as `gdb` is not the direct parent of the `bash` process being debugged](https://unix.stackexchange.com/a/403693/11938). – Eliah Kagan Nov 10 '17 at 09:00
  • Though `-q` and `-n` are helpful, they (namely `-q`) don't silence `gdb`, so the `/dev/null` redirect is still needed. Great suggestion though, @LucasCimon – fbicknel Dec 20 '17 at 20:12
  • any ideas how to do something similar on a machine without gdb? – lightswitch05 May 04 '18 at 18:40
  • 1
    @lightswitch05: see my ctypes.sh answer – Wil Jun 12 '18 at 21:21
  • you have my salute. I was constantly annoyed by this readonly TMOUT variable and was disappointed by my inability to `unset` it – Thamme Gowda Jul 03 '19 at 18:56
13

Edit 2023-08-29: Full rewrite, make them quicker and satisfy shellcheck

Shortly: inspired by anishsane's answer

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, then ${VARIABLE@a}...

destroy () { 
    declare -p "$1" &>/dev/null || return 1      # Return if variable not exist,
    local -n variable=$1
    local resline flags=${variable@a}
    local -i result
    case $flags in                  # Run gdb only in case variable is readonly.
        *r*) while read -r resline; do
                 case $resline in
                     "\$1 = "*)  result=" ${resline##*1 = } == 0 ? 0 : 1 ";;
                 esac
             done < <( exec gdb 2>&1 --pid=$$ --batch -ex \
                            "call (int) unbind_variable(\"$1\")" ) ;;
        *  ) unset "$1"; result=$? ;;
    esac
    return $result
}

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

Explanation:

 1        destroy () { 
 2            declare -p "$1" &>/dev/null || return 1      # Return if variable not exist,
 3            local -n variable=$1
 4            local resline flags=${variable@a}
 5            local -i result
 6            case $flags in                  # Don't run gdb if variable is not readonly.
 7                *r*) while read -r resline; do
 8                         case $resline in
 9                             "\$1 = "*)  result=" ${resline##*1 = } == 0 ? 0 : 1 ";;
10                         esac
11                     done < <( exec gdb 2>&1 --pid=$$ --batch -ex \
12                                    "call (int) unbind_variable(\"$1\")" ) ;;
13                *  ) unset "$1"; result=$? ;;
14            esac
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 6 to 14 will execute unset instead of gdb if readonly flag not present,
  • lines 7 to 10 while read ... result= ... done get result code of call (int) unbind_variable() from gdb output,
  • line 9 map result from 0 to 0 and from other than 0 to 1,
  • line 11-12 gdb syntax with use of --pid and --ex (see gdb --help),
  • line 15 return $result.

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 $?
    1
    
F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137
10

In zsh,

% typeset +r PI
% unset PI

(Yes, I know the question says bash. But when you Google for zsh, you also get a bunch of bash questions.)

henry
  • 4,244
  • 2
  • 26
  • 37
Resigned June 2023
  • 4,638
  • 3
  • 38
  • 49
8

Using GDB is terribly slow, or may even be forbidden by system policy (ie can't attach to process.)

Try ctypes.sh instead. It works by using libffi to directly call bash's unbind_variable() instead, which is every bit as fast as using any other bash builtin:

$ readonly PI=3.14
$ unset PI
bash: unset: PI: cannot unset: readonly variable

$ source ctypes.sh
$ dlcall unbind_variable string:PI

$ declare -p PI
bash: declare: PI: not found

First you will need to install ctypes.sh:

$ git clone https://github.com/taviso/ctypes.sh.git
$ cd ctypes.sh
$ ./autogen.sh
$ ./configure
$ make
$ sudo make install

See https://github.com/taviso/ctypes.sh for a full description and docs.

For the curious, yes this lets you call any function within bash, or any function in any library linked to bash, or even any external dynamically-loaded library if you like. Bash is now every bit as dangerous as perl... ;-)

Wil
  • 757
  • 9
  • 12
7

According to the man page:

   unset [-fv] [name ...]
          ...   Read-only  variables  may  not  be
          unset. ...

If you have not yet exported the variable, you can use exec "$0" "$@" to restart your shell, of course you will lose all other un-exported variables as well. It seems if you start a new shell without exec, it loses its read-only property for that shell.

Kevin
  • 53,822
  • 15
  • 101
  • 132
5

Specifically wrt to the TMOUT variable. Another option if gdb is not available is to copy bash to your home directory and patch the TMOUT string in the binary to something else, for instance XMOUX. And then run this extra layer of shell and you will not be timed out.

user1089933
  • 87
  • 1
  • 2
4
$ PI=3.17
$ export PI
$ readonly PI
$ echo $PI
3.17
$ PI=3.14
-bash: PI: readonly variable
$ echo $PI
3.17

What to do now?

$ exec $BASH
$ echo $PI
3.17
$ PI=3.14
$ echo $PI
3.14
$

A subshell can inherit the parent's variables, but won't inherit their protected status.

jezzaaaa
  • 91
  • 4
  • Thank you! This led to a simple approach to disabling TMOUT. Edit ~/.ssh/config Host section to have "RemoteCommand exec ${BASH}". – Les Grieve Jun 23 '21 at 16:21
3

readonly command makes it final and permanent until the shell process terminates. If you need to change a variable, don't mark it readonly.

Amit
  • 19,780
  • 6
  • 46
  • 54
3

An alternative if gdb is unavailable: You can use the enable command to load a custom builtin that will let you unset the read-only attribute. The gist of the code that does it:

SETVARATTR (find_variable ("TMOUT"), att_readonly, 1);

Obviously, you'd replace TMOUT with the variable you care about.

If you don't want to turn that into a builtin yourself, I forked bash in GitHub and added a fully-written and ready-to-compile loadable builtin called readwrite. The commit is at https://github.com/josephcsible/bash/commit/bcec716f4ca958e9c55a976050947d2327bcc195. If you want to use it, get the Bash source with my commit, run ./configure && make && make loadables to build it, then enable -f examples/loadables/readwrite readwrite to add it to your running session, then readwrite TMOUT to use it.

Community
  • 1
  • 1
2

No, not in the current shell. If you wish to assign a new value to it, you will have to fork a new shell where it will have a new meaning and will not be considered as read only.

$ { ( readonly pi=3.14; echo $pi ); pi=400; echo $pi; unset pi; echo [$pi]; }
3.14
400
[]
jaypal singh
  • 74,723
  • 23
  • 102
  • 147
1

You can't, from manual page of unset:

For each name, remove the corresponding variable or function. If no options are supplied, or the -v option is given, each name refers to a shell variable. Read-only variables may not be unset. If -f is specifed, each name refers to a shell function, and the function definition is removed. Each unset variable or function is removed from the environment passed to subsequent commands. If any of RANDOM, SECONDS, LINENO, HISTCMD, FUNCNAME, GROUPS, or DIRSTACK are unset, they lose their special properties, even if they are subsequently reset. The exit status is true unless a name is readonly.

Yu Hao
  • 119,891
  • 44
  • 235
  • 294
  • 2
    What I don't understand is why `typeset +r VAR` doesn't work since, also according to the man page, `Using '+' instead of '-' turns off the attribute instead, with the exception that +a may not be used to destroy an array variable.` – Trebor Rude Aug 13 '14 at 19:55
1

One other way to "unset" a read-only variable in Bash is to declare that variable read-only in a disposable context:

foo(){ declare -r PI=3.14; baz; }
bar(){ local PI=3.14; baz; }

baz(){ PI=3.1415927; echo PI=$PI; }

foo;

bash: PI: readonly variable

bar; 

PI=3.1415927

While this is not "unsetting" within scope, which is probably the intent of the original author, this is definitely setting a variable read-only from the point of view of baz() and then later making it read-write from the point of view of baz(), you just need to write your script with some forethought.

Wil
  • 757
  • 9
  • 12
1

Another solution without GDB or an external binary, (in fact an emphasis on Graham Nicholls comment) would be the use of exec.

In my case there were an annoying read-only variable set in /etc/profile.d/xxx.

Quoting the bash manual:

"When bash is invoked as an interactive login shell [...] it first reads and executes commands from the file /etc/profile" [...]

When an interactive shell that is not a login shell is started, bash reads and executes commands from /etc/bash.bashrc [...]

The gist of my workaround was to put in my ~/.bash_profile:

if [ -n "$annoying_variable" ]
then exec env annoying_variable='' /bin/bash
# or: then exec env -i /bin/bash
fi

Warning: to avoid a recursion (which would lock you out if you can only access your account through SSH), one should ensure the "annoying variable" will not be automatically set by the bashrc or to set another variable on the check, for example:

if [ -n "$annoying_variable" ] && [ "${SHLVL:-1}" = 1 ]
then exec env annoying_variable='' SHLVL=$((SHLVL+1)) ${SHELL:-/bin/bash}
fi
bufh
  • 3,153
  • 31
  • 36
0
$ readonly PI=3.14

$ unset PI
bash: PI: readonly variable

$ gdb --batch-silent --pid=$$ --eval-command='call (int) unbind_variable("PI")'

$ [[ ! -v PI ]] && echo "PI is unset ✔️"
PI is unset ✔️

Notes:

  1. Tested with bash 5.0.17 and gdb 10.1.
  2. The -v varname test was added in bash 4.2. It is "True if the shell variable varname is set (has been assigned a value)." – bash reference manual
  3. Note the cast to int. Without that, the following error will result: 'unbind_variable' has unknown return type; cast the call to its declared return type. The bash source code shows that the return type of the unbind_variable function is int.
  4. This answer is essentially the same as an answer over at superuser.com. I added the cast to int to get past the unknown return type error.
Robin A. Meade
  • 1,946
  • 18
  • 17
-1

if nothing helps, you could go back in time, to a time where readonly vars were not yet implemented:

env ENV=$HOME/.profile /bin/sh

and in $HOME/.profile show some good will and say

export TMOUT=901

This gives you one extra second before you are logged out :-)