繁体   English   中英

在 bash 中取消设置只读变量

[英]Unset readonly variable in bash

如何取消设置 Bash 中的只读变量?

$ readonly PI=3.14

$ unset PI
bash: PI: readonly variable

还是不可能?

实际上,您可以取消设置只读变量 但我必须警告,这是一种骇人听闻的方法。 添加此答案,仅作为信息,而不是作为建议。 使用它需要您自担风险。 在 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

根据内核的 ptrace_scope 设置,可能需要也可能不需要sudo 查看有关 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

在 zsh 中,

% typeset +r PI
% unset PI

(是的,我知道这个问题说的是 bash。但是当你谷歌搜索 zsh 时,你也会得到一堆 bash 问题。)

根据手册页:

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

如果你还没有导出变量,你可以使用exec "$0" "$@"重新启动你的 shell,当然你也会丢失所有其他未导出的变量。 似乎如果你在没有exec情况下启动一个新的 shell,它会失去该 shell 的只读属性。

使用 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 一样危险...... ;-)

很快:灵感来自anishsane 的回答

但使用更简单的语法:

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

随着一些改进,作为一个功能:

我的destroy功能:

如何使用可变元数据 注意罕见的bashisms用法: local -n VARIABLE=$1${VARIABLE@a} ...

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
}

您可以将其复制到名为destroy.bashbash 源文件中,例如...

解释:

 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 }
  • 第 2 行创建提交变量的本地引用
  • 第 3 行防止在不存在的变量上运行
  • 第 4 行将参数的属性(元)存储到$flags
  • 如果readonly 标志不存在,第 5 到 8 行将运行unset而不是gdb
  • 第 9 行到第 12 行while read ... result= ... donegdb输出中获取call unbind返回码
  • 使用--pid--ex第 13 行gdb语法(请参阅gdb --help )。
  • 第 15 行返回call unbind命令的$result

使用中:

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

特别是写入 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 进程终止。 如果您需要更改变量,请不要将其标记为只读。

不,不在当前的 shell 中。 如果你想给它分配一个新的值,你必须创建一个新的 shell,它会有一个新的含义并且不会被认为是read only

$ { ( readonly pi=3.14; echo $pi ); pi=400; echo $pi; unset pi; echo [$pi]; }
3.14
400
[]

如果 gdb 不可用,另一种方法是:您可以使用enable命令加载一个自定义内置函数,它可以让您取消设置只读属性。 执行此操作的代码要点:

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

显然,您可以用您关心的变量替换TMOUT

如果您不想自己将其转换为内置函数,我在 GitHub 中分叉了 bash 并添加了一个完整编写且准备编译的可加载内置readwrite 提交位于https://github.com/josephcsible/bash/commit/bcec716f4ca958e9c55a976050947d2327bcc195 如果你想使用它,用我的提交获取 Bash 源,运行./configure && make loadables来构建它,然后enable -f examples/loadables/readwrite readwrite将它添加到你的运行会话,然后readwrite TMOUT使用它.

你不能,从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 ✔️

笔记:

  1. 使用bash 5.0.17gdb 10.1测试。
  2. -v varname测试是在bash 4.2中添加的。 “如果 shell 变量varname已设置(已分配值),则为True 。” bash 参考手册
  3. 注意转换为int 否则,将导致以下错误: 'unbind_variable' has unknown return type; cast the call to its declared return type 'unbind_variable' has unknown return type; cast the call to its declared return type bash 源代码显示unbind_variable函数的返回类型是int
  4. 这个答案基本上与superuser.com 上的答案相同。 我将强制转换添加到int以克服unknown return type错误。

如果没有任何帮助,您可以 go 回到过去,回到尚未实现只读变量的时间:

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

并在 $HOME/.profile 中表现出一些善意并说

export TMOUT=901

这会在您注销前多给您一秒钟:-)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM