4

我的意思是我想使用unset的不是 shell 函数本身。如果我能做到这一点,我可以通过运行来确保它command是纯粹的

#!/bin/sh
{ \unset -f unalias command [; \unalias unset command [ } 2>/dev/null;
# make zsh find *builtins* with `command` too:
[ -n "$ZSH_VERSION" ] && options[POSIX_BUILTINS]=on

如果我使用的是 Debian Almquist shell (dash),我想我可以相信它\unset是纯的。至少我无法定义名为unsetin的 shell 函数dash。而 inbash或 inzsh我可以定义unset() { echo fake unset; },此后我无法取消设置该功能:\unset -f unset输出“假未设置”。

与此相关,在bash脚本中,可以导出一个函数,export -f <function name>以便在bash脚本调用的脚本中使用它。dash但是,这在脚本中不起作用。我想知道,如果我使用的是,我是否必须担心一个命令被定义为我正在编写的脚本文件之外的 shell 函数dash?其他 POSIX 兼容的 shell 怎么样?

4

3 回答 3

12

注意:以下适用于所有主要的 POSIX 兼容 shell,除非另有说明:bashdashkshzsh. ( ,Debian Almquist Shell,是基于 Debian 的 Linux 发行版(例如 Ubuntu)上dash的默认 shell ( )。sh

  • unset具有其原始含义-可以使用其选项取消定义 shell函数-f的内置命令- 是确保任何其他 shell 关键字、命令或内置命令具有其原始含义的关键。

    • 从未修改的 开始unset,您可以确保未修改shopt和/或command,它们一起可以用来绕过或取消定义可能影响 shell 关键字、内置函数和外部实用程序的任何别名或 shell 函数。
    • 作为未定义函数的替代方法,command可用于通过环境绕过它们,包括那些可能已在代码之外定义的函数;仅支持
      导出功能只是这些机制之一;不同的外壳有不同的外壳,可能支持多个 - 见下文。bash
  • 只有dash,kshbash when 在 POSIX 兼容模式下保证unset没有重新定义:

    • dash并且ksh是安全的,因为它们不允许定义名为的函数unset,正如您所发现的,并且可以通过调用 as 来绕过任何别名形式\unset

    • bash当处于 POSIX 兼容模式时,允许您定义一个名为 的函数unset,但在您调用时忽略unset它,并且始终执行内置函数,正如您稍后发现的那样。

      • 鉴于POSIX 兼容模式限制了 Bash 的功能集并修改了其行为,通常不希望在其中运行 Bash 代码。在这篇文章的底部是您建议的解决方法的实现,它暂时激活 POSIX 兼容模式以确保未定义任何unset 函数
  • 可悲的是,据我所知,在zsh- 以及bash默认模式下 -无法保证unset自身没有被重新定义,并且可能还有其他类似 POSIX 的 shell 的行为类似。

    • 将其称为\unset(引用名称的任何部分)将绕过别名重新定义,但不会绕过函数重新定义 - 并且要撤消您需要原来的unset本身:catch 22。
  • 因此,在无法控制执行环境的情况下,您无法编写完全不受篡改的 shell 脚本除非您知道您的代码将由dashkshbash(使用解决方法)执行。

    • 如果您愿意假设unset没有被篡改,最可靠的方法是:

      • 用于\unset -f确保unaliascommand未被修改(不被 shell 函数遮蔽\unset -f unalias command:)

        • 函数,与别名不同,必须通过名称显式地未定义,但不幸的是,并非所有 shell 都提供枚举所有已定义函数的机制(typeset -f适用于bashkshzsh,但dash似乎根本没有机制),因此未定义所有函数是并不总是可能的。
      • 用于\unalias -a删除所有别名。

      • 然后使用 调用所有内容定义command [-p]的函数除外。调用外部实用程序时,尽可能使用显式路径,和/或在标准实用程序的情况下,使用限制在标准位置的最小定义(运行以查看该定义)。command -p$PATHcommand -p getconf PATH


附加信息:

  • 根据 POSIX,引用命令名称的任何部分(例如,\unset)会绕过该名称的任何别名形式或关键字形式(POSIX 和用语中的保留字zsh) - 但不是shell函数

  • 根据 POSIX,unalias -a取消定义所有别名。没有等效的、符合 POSIX 的命令来取消定义所有函数

    • 警告:旧zsh版本支持-a;但是,至少从 开始v5.0.8,它们确实如此。
  • Builtincommand可用于绕过, 和- 中的关键字、别名、函数bash,换句话说:仅执行builtins外部实用程序。相比之下,默认情况下也会绕过内置函数;要执行内置函数,请使用. dashkshcommandzshzshoptions[POSIX_BUILTINS]=on

  • 以下可用于在所有 shell 中执行仅命名的外部实用程序: 请注意,虽然它不是 POSIX 实用程序,但它在现代类 Unix 平台上广泛可用。<name>
    "$(command which <name>)" ...
    which

  • 命令形式的优先级:

    • bash, zsh: 别名 > shell 关键字 > shell 函数 > 内置 > 外部实用程序
    • ksh, dash: shell 关键字 > 别名 > shell 函数 > 内置 > 外部实用程序
    • 即: inbashzsh别名可以覆盖 shell 关键字,而 inkshdash它不能。
  • bash, ksh, 和zsh- 但不是dash- 都允许使用非标准函数签名 ,function <name> { ...作为 POSIX 兼容<name>() { ...形式的替代方案。

    • 语法是以下条件的function先决条件:
      • 确保在定义函数之前<name>它本身不受别名扩展的影响。
      • 能够选择一个<name>也是 shell关键字的;
        请注意,这样的函数只能以带引号的形式调用;例如,\while
      • (在 的情况下ksh,使用function语法还意味着typeset语句创建局部变量。)
    • dash, ksh, 和bash 当处于 POSIX 模式下时,还会额外阻止特殊内置函数的命名函数(例如,, unset, break, setshift;可以在此处找到 POSIX 定义的特殊内置函数列表;bothdashksh添加一些不能重新定义的更多内容(例如,localin dash;typesetunaliasin ksh),但是两个 shell 都有可以重新定义的额外的非特殊内置函数(例如,)。 请注意,在上述规则的情况下,无论是否使用语法都适用。type
      kshfunction
  • 您的代码范围内的环境 shell 函数的潜在来源:

    • 注意:防止这些问题的最简单方法是在您想要调用内置或外部实用程序时使用(未修改的)command内置(在zshwith中,以防止绕过内置)。options[POSIX_BUILTINS]=on

    • ENVPOSIX 要求将环境变量中由其绝对路径指定的脚本作为交互式shell的来源(有一些限制 - 请参阅规范);并始终尊重这一点,而仅在以 or 调用时才这样做,在 v4.2+ 中,使用; 相反,从不尊重这个变量。kshdashbashsh--posixzsh

      • 注意:作为脚本运行的代码通常会在非交互式shell 中运行,但这不能保证;例如,您的代码可能来自交互式脚本,或者有人可以调用您的脚本,例如sh -i强制交互式实例。
    • bash2个机制:

      • 使用or导出单个函数(其他 shell 仅支持导出变量export -fdeclare -fx
      • 每当在可选环境变量中启动非交互式shell时,指定要源的脚本的完整路径BASH_ENV
    • ksh支持通过可选的环境变量自动加载FPATH函数:包含位于任何指定目录中的函数定义的文件FPATH隐式自动加载。

      • (也zsh支持FPATH,但是自动加载函数需要一个明确 autoload <name>的声明,所以除非你特别要求一个给定名称的函数来自动加载,否则不会将任何函数添加到你的 shell 中。)
    • zsh支持通过其和初始化文件为任何 zsh实例(无论是否交互)采购脚本。/etc/zshenv~/.zhsenv

    • dash似乎不支持通过环境定义功能的任何机制。)


解决方法bash:确保unset具有其原始含义:

仅当您知道bash将执行您的脚本时,此解决方法才是安全的,不幸的是,它本身不能得到保证。

此外,因为它修改了 shell 环境(删除别名和函数),所以它不适合设计为 source的脚本。

如前所述,通常不希望在 Bash 的 POSIX 兼容模式下运行您的代码,但您可以临时激活它以确保它unset不会被函数所掩盖:

#!/bin/bash

# *Temporarily* force Bash into POSIX compatibility mode, where `unset` cannot 
# be shadowed, which allows us to undefine any `unset` *function* as well
# as other functions that may shadow crucial commands.
# Note: Fortunately, POSIXLY_CORRECT= works even without `export`, because
#       use of `export` is not safe at this point.
#       By contrast, a simple assignment cannot be tampered with.
POSIXLY_CORRECT=

# If defined, unset unset() and other functions that may shadow crucial commands.
# Note the \ prefix to ensure that aliases are bypassed.
\unset -f unset unalias read declare

# Remove all aliases.
# (Note that while alias expansion is off by default in scripts, it may
#  have been turned on explicitly in a tampered-with environment.)
\unalias -a  # Note: After this, \ to bypass aliases is no longer needed.

# Now it is safe to turn POSIX mode back off, so as to reenable all Bash
# features.
unset POSIXLY_CORRECT

# Now UNDEFINE ALL REMAINING FUNCTIONS:
# Note that we do this AFTER switching back from POSIX mode, because
# Bash in its default mode allows defining functions with nonstandard names
# such as `[` or `z?`, and such functions can also only be *unset* while
# in default mode.
# Also note that we needn't worry about keywords `while`, `do` and `done`
# being shadowed by functions, because the only way to invoke such functions
# (which you can only define with the nonstandard `function` keyword) would
# be with `\` (e.g., `\while`).
while read _ _ n; do unset -f "$n"; done < <(declare -F)

# IN THE REST OF THE SCRIPT:
#  - It is now safe to call *builtins* as-is.
#  - *External utilities* should be invoked:
#      - by full path, if feasible
#      - and/or, in the case of *standard utilities*, with
#        command -p, which uses a minimal $PATH definition that only
#        comprises the locations of standard utilities.
#      - alternatively, as @jarno suggests, you can redefine your $PATH
#        to contain standard locations only, after which you can invoke
#        standard utilities by name only, as usual:
#          PATH=$(command -p getconf PATH)

# Example command:
# Verify that `unset` now refers to the *builtin*:
type unset

测试命令:

假设上面的代码被保存到script当前目录的文件中。

unset以下命令模拟了一个被别名和函数遮蔽的篡改环境,并且 filescriptsourced,导致它看到函数,并且在交互式获取时也扩展别名:

$ (unset() { echo hi; }; alias unset='echo here'; . ./script)
unset is a shell builtin

type unset输出unset is a shell builtin证明该函数和隐藏内置函数的别名unset均已停用。

于 2016-03-11T03:52:34.227 回答
1

有趣的是,你已经说过内置名称——command

$ var="FOO"
$ unset() { echo nope; }
$ echo "${var}"
FOO
$ unset var
nope
$ echo "${var}"
FOO
$ command unset var
$ echo "${var}"
<nothing!>

如果您处于有人创建command() { :; }函数的敌对环境中,这将无济于事。但是,如果您处于敌对环境中,那么您已经迷路了;)。

在将函数导出到环境中时,这是一个特定于 bash 的扩展,您不应该真正依赖它。POSIX shell(如破折号)不支持这种设计。

于 2016-03-10T14:35:15.353 回答
1

这就是我知道的可以做的...

#!/bin/bash --posix

# 如果设置了例如 BASH_FUNC_unset() 环境变量,则无法执行脚本
# 走到这一步(前提是它按原样运行,而不是作为`bash script ...`)

unset -f 内置命令声明 ...

已保存_IFS=$IFS;只读保存_IFS
# 删除所有函数(shell 内置声明在子shell 中执行)
IFS=$'\n'; 对于 f in `declare -Fx`; 取消设置 -f ${f##* }; 完毕; IFS=$saved_IFS
于 2016-11-01T10:32:49.980 回答