使用管道命令的 eval 限制

hel*_*ode 5 shell bash pipe

我们有一个 shell 脚本,它在一个变量中构建了一个长管道命令链,并使用 eval 执行它(以下代码简化为基本代码):

 cmd="cat /some/files | grep -v \"this\" | grep -v \"that\""
 cmd="$cmd | grep -v \"much more dynamical filter with variables\""
 ...
 result=`eval $cmd`
Run Code Online (Sandbox Code Playgroud)

到目前为止一切正常,但现在似乎 cmd 变量的内容超出了限制。当它超过大约 95970 字节时,我将收到错误(尽管语法是正确的):

eval: line ...: syntax error near unexpected token `|'
Run Code Online (Sandbox Code Playgroud)

我做了一些研究,但我没有得到任何线索(getconf ARG_MAX echoes 2621440,ulimit -a 也没有帮助我)。

有人可以解释一下这可能是哪个限制,也许如何增加限制或避免它的最佳方法是什么?


编辑:我现在用指定的脚本在三个不同的服务器(centos)上测试了它。在所有服务器上,我最终使用 eval 在一个命令中达到了 3333 个管道。

我找到了另一个页面,其中有人经历了相同但没有 eval 的经历。所以它似乎只是管道的限制。

知道限制可能是由管道数量引起的将帮助我解决这个问题。所以这不再是问题了。

但是我仍然对如何设置此限制或至少如何在不为此运行脚本的情况下检测限制值(可能不是在每个系统 3333 上)感兴趣。

它可以通过以下方式复制:

yes cat | head -n 3334 | paste -sd '|' - | bash
Run Code Online (Sandbox Code Playgroud)

ric*_*ici 10

这里的问题实际上是bash解析器的问题。除了编辑和重新编译之外没有其他解决方法bash,并且 3333 限制可能在所有平台上都相同。

bash 解析器是用yacc(或者,通常,用bison但在yacc模式下)生成的。yacc解析器是自底向上的解析器,使用 LALR(1) 算法构建带有下推堆栈的有限状态机。粗略地说,堆栈包含所有尚未减少的符号,以及足够的信息来决定使用哪些产生式来减少符号。

此类解析器针对左递归语法规则进行了优化。在表达式文法的上下文中,左递归规则适用于左结合运算符,例如普通数学中的a - b。这是左结合的,因为表达式abc将(“关联”)分组到左侧,使其等于 ( ab )−c 而不是a −( bc )。相比之下,求幂是右结合的,因此按照惯例a b c被评估为a ( b c )而不是 (a b ) c .

bash运算符是过程运算符,而不是算术运算符;这些包括短路布尔值(&&||)和管道(||&),以及排序运算符;&。像数学运算符一样,这​​些运算符中的大多数都与左侧相关联,但管道运算符与右侧相关联,因此将cmd1 | cmd2 | cmd3其解析为cmd1 | { cmd2 | cmd3 ; }{ cmd1 | cmd2 ; } | cmd3. (大多数时候差异并不重要,但它是可观察到的。[见注 1])

要解析作为左关联运算符序列的表达式,您只需要一个小的解析器堆栈。每次点击运算符时,您都可以减少(括号,如果你喜欢)它左边的表达式。相比之下,解析一个右结合运算符序列的表达式需要您将所有符号放入解析器堆栈,直到到达表达式的末尾,因为只有这样您才能开始减少(插入括号)。(该解释涉及相当多的挥手,因为它旨在是非技术性的,但它基于实际算法的工作。)

Yacc 解析器会在运行时调整它们的解析器堆栈的大小,但编译时有一个最大堆栈大小,默认情况下为 10000 个插槽。如果堆栈达到最大大小,任何扩展它的尝试都会触发内存不足错误。因为|是右结合,形式的表达:

statement | statement | ... | statement 
Run Code Online (Sandbox Code Playgroud)

最终会触发这个错误。如果以明显的方式解析它,那将在 5,000 个管道符号(带有 5,000 个语句)之后发生。但是由于bash解析器处理换行符的方式,实际使用的语法是(大致):

pipeline: command '|' optional_newlines pipeline
Run Code Online (Sandbox Code Playgroud)

结果是optional_newlines|each之后有一个语法符号,所以每个管道占用三个堆栈槽。因此,内存不足错误是在 3,333 个管道符号后生成的。

yacc 解析器检测堆栈溢出并发出信号,它通过调用yyerror("memory exhausted"). 但是,bash实现yyerror丢弃了提供的错误消息,并替换为“在意外标记附近检测到语法错误...”之类的消息。在这种情况下,这有点令人困惑。


笔记

  1. 使用|&操作符最容易观察到结合性的差异,该操作符同时传输 stderr 和 stdout。(或者,更准确地说,在建立管道后将 stdout 复制到 stderr。)举一个简单的例子,假设foo当前目录中不存在该文件。然后

    # There is a race condition in this example. But it's not relevant.
    $ ls foo | ls foo |& tr n-za-m a-z
    ls: cannot access foo: No such file or directory
    yf: pnaabg npprff sbb: Nb fhpu svyr be qverpgbel
    # Associated to the left:
    $ { ls foo | ls foo ; } |& tr n-za-m a-z
    yf: pnaabg npprff sbb: Nb fhpu svyr be qverpgbel
    yf: pnaabg npprff sbb: Nb fhpu svyr be qverpgbel
    # Associated to the right:
    $ ls foo | { ls foo |& tr n-za-m a-z ; }
    ls: cannot access foo: No such file or directory
    yf: pnaabg npprff sbb: Nb fhpu svyr be qverpgbel
    
    Run Code Online (Sandbox Code Playgroud)

  • @mikeserv:不一定。我不知道如何处理其他 shell 和 bash 的源代码;我知道 zsh 使用手工构建的解析器,它可能没有相同的限制。如果输入足够复杂,任何解析器最终都会耗尽内存;唯一真正的问题是它如何体现问题(以及,如果它受到控制,在什么时候。) (2认同)