Unset readonly variable in bash

让人想犯罪 __ 提交于 2019-11-26 23:58:54
anishsane

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 recommendation. Use 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

$

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

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.

Shortly: inspired by anishsane's answer

But with simplier syntax:

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

With some improvement, as a function:

My destroy function:

Or How to check variable meta data...

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

In use:

source 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

# now 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

# and with non existant variable
destroy PI
echo $?
255

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

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
[]

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.

In zsh,

$ typeset +r PI

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

Using GDB is terribly slow. 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... ;-)

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.

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.

$ 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.

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!