如何在 Bash 中取消设置只读变量?
$ readonly PI=3.14
$ unset PI
bash: PI: readonly variable
还是不可能?
实际上,您可以取消设置只读变量。但我必须警告这是一种 hacky 方法。添加此答案,仅作为信息,不作为建议。需要您自担风险使用它。在 ubuntu 13.04、bash 4.2.45 上测试。
此方法涉及了解一些 bash 源代码,并且它继承自此答案。
$ 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
$
oneliner 的答案是使用批处理模式和其他命令行标志,如F. Hauri 的回答中所提供:
$ sudo gdb -ex 'call unbind_variable("PI")' --pid=$$ --batch
sudo
根据内核的 ptrace_scope 设置,可能需要也可能不需要。查看有关 vip9937 答案的评论以获取更多详细信息。
我尝试了上面的 gdb hack,因为我想取消设置 TMOUT(禁用自动注销),但是在将 TMOUT 设置为只读的机器上,我不允许使用 sudo。但由于我拥有 bash 进程,我不需要 sudo。但是,语法在我使用的机器上不太适用。
不过,这确实有效(我把它放在我的 .bashrc 文件中):
# 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
使用 GDB 非常慢,甚至可能被系统策略禁止(即无法附加到进程。)
请改用 ctypes.sh。它通过使用 libffi 直接调用 bash 的 unbind_variable() 来工作,这与使用任何其他 bash 内置函数一样快:
$ 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
首先你需要安装 ctypes.sh:
$ git clone https://github.com/taviso/ctypes.sh.git
$ cd ctypes.sh
$ ./autogen.sh
$ ./configure
$ make
$ sudo make install
有关完整描述和文档,请参阅https://github.com/taviso/ctypes.sh。
出于好奇,是的,这可以让您调用 bash 中的任何函数,或者任何链接到 bash 的库中的任何函数,或者如果您愿意,甚至可以调用任何外部动态加载的库。Bash 现在和 perl 一样危险...... ;-)
编辑 2021-11-10:添加(int)
到cast unbind_variable
结果中。
但语法更简单:
$ gdb -ex 'call (int) unbind_variable("PI")' --pid=$$ --batch
通过一些改进,作为一个功能:
destroy
功能:或如何使用可变元数据。注意罕见的bashisms的用法:local -n VARIABLE=$1
和${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
}
您可以将其复制到名为的bash 源文件destroy.bash
中,例如...
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 }
$flags
.unset
而不是gdb
while read ... result= ... done
获取call (int) unbind_variable()
ingdb
输出的返回码gdb
使用--pid
and的第 13 行语法--ex
(请参阅 参考资料gdb --help
)。$result
命令unbind_variable()
返回。$ . destroy.bash
第一个使用任何常规(读写)变量:
$ 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
第二个只读变量:
$ 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
第三个不存在的变量:
$ destroy PI
$ echo $?
255
在 zsh 中,
% typeset +r PI
% unset PI
(是的,我知道这个问题是 bash。但是当你用谷歌搜索 zsh 时,你也会得到一堆 bash 问题。)
根据手册页:
unset [-fv] [name ...]
... Read-only variables may not be
unset. ...
如果您还没有导出变量,您可以使用exec "$0" "$@"
重新启动您的 shell,当然您也会丢失所有其他未导出的变量。似乎如果您在没有 的情况下启动新的 shell exec
,它会丢失该 shell 的只读属性。
具体到 TMOUT 变量。如果 gdb 不可用,另一种选择是将 bash 复制到您的主目录并将二进制文件中的 TMOUT 字符串修补为其他内容,例如 XMOUX。然后再运行这层额外的shell,你就不会超时了。
$ PI=3.17
$ export PI
$ readonly PI
$ echo $PI
3.17
$ PI=3.14
-bash: PI: readonly variable
$ echo $PI
3.17
现在要做什么?
$ exec $BASH
$ echo $PI
3.17
$ PI=3.14
$ echo $PI
3.14
$
子shell 可以继承父级的变量,但不会继承它们的受保护状态。
readonly 命令使其成为最终的和永久的,直到 shell 进程终止。如果您需要更改变量,请不要将其标记为只读。
如果 gdb 不可用,另一种选择:您可以使用该enable
命令加载自定义内置命令,该命令可让您取消设置只读属性。执行此操作的代码要点:
SETVARATTR (find_variable ("TMOUT"), att_readonly, 1);
显然,您将替换TMOUT
为您关心的变量。
如果你不想自己把它变成一个内置的,我在 GitHub 中 fork bash 并添加了一个完全编写和准备编译的可加载内置,称为readwrite
. 提交位于https://github.com/josephcsible/bash/commit/bcec716f4ca958e9c55a976050947d2327bcc195。如果您想使用它,请使用我的提交获取 Bash 源代码,运行./configure && make && make loadables
以构建它,然后enable -f examples/loadables/readwrite readwrite
将其添加到您正在运行的会话中,然后readwrite TMOUT
使用它。
不,不在当前的外壳中。如果你想给它分配一个新的值,你将不得不派生一个新的外壳,它将具有新的含义,并且不会被视为read only
.
$ { ( readonly pi=3.14; echo $pi ); pi=400; echo $pi; unset pi; echo [$pi]; }
3.14
400
[]
你不能,从手册页unset
:
对于每个名称,删除相应的变量或函数。如果未提供任何选项,或者给出了 -v 选项,则每个名称都指向一个 shell 变量。 只读变量不能取消设置。 如果指定了 -f,则每个名称都引用一个 shell 函数,并删除函数定义。每个未设置的变量或函数都会从传递给后续命令的环境中删除。如果 RANDOM、SECONDS、LINENO、HISTCMD、FUNCNAME、GROUPS 或 DIRSTACK 中的任何一个未设置,即使它们随后被重置,它们也会失去其特殊属性。除非名称是只读的,否则退出状态为真。
在 Bash 中“取消设置”只读变量的另一种方法是在一次性上下文中将该变量声明为只读:
foo(){ declare -r PI=3.14; baz; }
bar(){ local PI=3.14; baz; }
baz(){ PI=3.1415927; echo PI=$PI; }
foo;
bash: PI: 只读变量
bar;
PI=3.1415927
虽然这不是在范围内“取消设置”,这可能是原作者的意图,但这绝对是从 baz() 的角度将变量设置为只读,然后从从 baz() 的角度来看,您只需要深思熟虑地编写脚本。
另一个没有GDB或外部二进制文件的解决方案(实际上是对Graham Nicholls评论的强调)是使用exec
.
在我的例子中,有一个烦人的只读变量设置在/etc/profile.d/xxx
.
引用 bash 手册:
“当 bash 作为交互式登录 shell 调用时 [...] 它首先从文件 /etc/profile 读取并执行命令” [...]
当一个不是登录 shell 的交互式 shell 启动时,bash 读取并执行来自 /etc/bash.bashrc [...]
我的解决方法的要点是放入我的~/.bash_profile
:
if [ -n "$annoying_variable" ]
then exec env annoying_variable='' /bin/bash
# or: then exec env -i /bin/bash
fi
警告:为避免递归(如果您只能通过 SSH 访问您的帐户,则会将您锁定),应确保 bashrc 不会自动设置“烦人的变量”或在支票上设置另一个变量,例如:
if [ -n "$annoying_variable" ] && [ "${SHLVL:-1}" = 1 ]
then exec env annoying_variable='' SHLVL=$((SHLVL+1)) ${SHELL:-/bin/bash}
fi
$ 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 ✔️
笔记:
bash 5.0.17
和测试gdb 10.1
。-v varname
测试已添加到bash 4.2
. 它是“True
如果设置了 shell 变量varname
(已被赋值)。” – bash 参考手册int
. 否则,将导致以下错误:'unbind_variable' has unknown return type; cast the call to its declared return type
. bash 源代码显示该函数的返回类型unbind_variable
为int
.int
克服unknown return type
错误。