【问题标题】:In Bash, is it possible to write an 'identity' function (relative to arguments passing/returning)在 Bash 中,是否可以编写一个“身份”函数(相对于参数传递/返回)
【发布时间】:2026-02-05 21:05:02
【问题描述】:

这样的“身份”功能应满足以下两个属性:

identity $(identity a\ b c\ d)
# (Expected output:)
# a b
# c d

并且,给定以下 'argv_count' 函数:

argv_count () { echo "argv_count('$@'):$#"; }
argv_count $(identity a\ b c\ d)
# (Expected output:)
# argv_count('a b c d'):2

如果需要,可以在测试中引入额外的引号。

一个简单的候选人如以下未能通过第二次测试:

identity () { for arg in "$@"; do echo "$arg"; done; }

cat 不是正确的解决方案,因为它是相对于 stdin|stdout 的恒等函数。

【问题讨论】:

    标签: bash variables arguments quotes expansion


    【解决方案1】:

    如果您愿意使用eval,您可以解决返回值的分词问题:

    $ argv_count a\ b c\ d
    argv_count('a b c d'):2
    $ identity() { printf ' %q' "$@"; }
    $ eval argv_count "$(identity a\ b c\ d)"
    argv_count('a b c d'):2
    $ eval argv_count "$(eval identity "$(identity a\ b c\ d)")"
    argv_count('a b c d'):2
    

    或者 Gordon Davisson 的棘手案例:

    $ argv_count $'foo\t * bar'
    argv_count('foo  * bar'):1
    $ eval argv_count "$(eval identity "$(identity $'foo\t * bar')")"
    argv_count('foo  * bar'):1
    

    【讨论】:

    • 在一些奇怪的情况下这不起作用——例如尝试eval argv_count $(identity $'foo\t * bar')。我想如果你在它周围添加双引号,比如"$(identity ....)",它会解决这些情况。
    • @GordonDavisson 迷人的例子!我不知道为什么eval argv_count $(eval identity $(identity $'foo\t * bar')) 可以正常工作,但添加\t 会破坏更简单的:eval argv_count $(identity $'foo\t * bar')。至于双引号作为修复,我相信它会破坏可嵌套性。
    • @GordonDavisson 顺便说一句,我赞成你的正确答案。我只是出于求知欲而添加了我的答案。
    • 实际上,我认为你的 kluge 比我建议的要好。更复杂的版本对我来说失败了,就像更简单的版本一样;不知道为什么它对你有用,但双引号似乎嵌套很好,你只需要在每个级别引用:eval argv_count "$(eval identity "$(identity $'foo\t * bar')")" 按预期工作。
    • @GordonDavisson 神圣的烟雾确实有效!我不知道我昨天打错了什么以获得不同的结果。
    【解决方案2】:

    不,这是不可能的。原因是当 shell 解析命令替换的输出(即$(somecommand))时,它会执行分词和通配符扩展但没有引号或转义评估。这意味着如果identity 的输出包含一个空格,shell 将其视为“单词”(即其他程序的参数)之间的分隔符,无论您使用什么引号/转义/什么添加以尽量避免这种情况。更糟糕的是,输出中的任何包含通配符的单词扩展为匹配文件的列表(如果有的话)。这意味着$(identity 'foo * bar') 注定要失败。

    话虽如此,有一些方法可以通过更改 shell 设置来伪造它。例如,set -f 将关闭通配符扩展,从而解决了这个问题——除了你必须在运行identity 之前在父 shell 中设置它,然后再将其设置为正常,否则很多其他事情会中断。同样,您可以更改 IFS 以防止分词将空格视为分隔符 - 但您必须再次在父 shell 中更改它并在之后将其设置回来,并且然后它会造成麻烦对于您选择的任何替换分隔符。所以你可以伪造它,但这是非常糟糕的伪造。

    编辑:正如 Michael Kropat 指出的那样,使用 eval 是另一种“伪造”它的方式,如果仔细使用会更加灵活。

    【讨论】:

    • 感谢你们两位的解释!在某些实际情况下替换它:这是否意味着没有函数可以安全地返回文件名列表(可能包含空格)作为输出?因为除非在调用者中使用eval,否则您将失去空格和分隔符之间的区别,因此以后无法遍历此文件列表。
    • @LucasCimon: 一个函数可以返回一个由空字符分隔的列表(就像find -print0 所做的那样),但这不能与$( ) 构造一起使用——你必须将它传递给类似的东西xargs -0while IFS= read -rd '' argument 循环。这是“安全”的原因与您不能将其与 $( ) 一起使用的原因相同:C 字符串不能包含空值,因此参数和 $( ) 都不能处理它们。