我们有一个 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`
到目前为止一切正常,但现在 cmd 变量的内容似乎超出了限制。当它超过大约 95970 字节时,我将收到错误(尽管语法是正确的):
eval: line ...: syntax error near unexpected token `|'
我做了一些研究,但我没有得到任何线索(getconf ARG_MAX 回显 2621440,ulimit -a 也对我没有帮助)。
有人可以解释一下这可能是哪个限制以及如何增加限制或者避免它的最佳方法是什么?
编辑:我现在已经使用指定的脚本在三个不同的服务器(centos)上测试了它。在所有服务器上,我最终使用 eval 在一个命令中达到了 3333 个管道。
我发现另一页有人经历过同样的事情但没有评估。所以这似乎只是管道的限制。
知道限制可能是由管道数量引起的将帮助我解决该问题。所以这不再是问题了。
但我仍然对如何设置此限制或至少如何在不运行脚本的情况下检测限制值(可能不是在每个系统 3333 上)感兴趣。
可以使用以下方法复制:
yes cat | head -n 3334 | paste -sd '|' - | bash
答案1
这里的问题实际上是解析器的问题bash
。除了编辑和重新编译之外没有其他解决方法bash
,并且 3333 限制在所有平台上可能都是相同的。
bash 解析器是用yacc
(或者通常是用bison
but inyacc
模式)生成的。yacc
解析器是自下而上的解析器,使用 LALR(1) 算法构建带有下推堆栈的有限状态机。宽松地说,堆栈包含所有尚未约简的符号,以及足够的信息来决定使用哪些产生式来约简符号。
此类解析器针对左递归语法规则进行了优化。在表达式语法的上下文中,左递归规则适用于左结合运算符,例如A-乙在普通数学中。这是左结合的,因为表达式A-乙-C向左分组(“关联”),使其等于 (A-乙)−c 而不是A−(乙-C)。相比之下,求幂是右结合的,因此A乙C按照惯例评估为A(乙C)而不是 (A乙)C。
bash
运算符是过程运算符,而不是算术运算符;其中包括短路布尔值 ( &&
and ||
) 和管道 ( |
and |&
),以及排序运算符;
和&
。与数学运算符一样,大多数运算符与左侧关联,但管道运算符与右侧关联,因此cmd1 | cmd2 | cmd3
将其解析为cmd1 | { cmd2 | cmd3 ; }
与 相对{ cmd1 | cmd2 ; } | cmd3
。 (大多数时候,差异并不重要,但可以观察到。[参见注释 1])
要解析由左关联运算符序列组成的表达式,您只需要一个小的解析器堆栈。每次点击运算符时,您都可以减少(如果愿意,可以添加括号)其左侧的表达式。相比之下,解析由右结合运算符序列组成的表达式需要将所有符号放入解析器堆栈中,直到到达表达式末尾,因为只有那时才能开始减少(插入括号)。 (这种解释涉及相当多的挥手,因为它的目的是非技术性的,但它是基于真实算法的工作原理。)
Yacc 解析器将在运行时调整其解析器堆栈的大小,但有编译时最大堆栈大小,默认情况下为 10000 个槽。如果堆栈达到最大大小,任何扩展堆栈的尝试都会触发内存不足错误。因为|
是右结合词,其表达式如下:
statement | statement | ... | statement
最终会触发这个错误。如果以明显的方式对其进行解析,那么这将在 5,000 个管道符号(带有 5,000 个语句)之后发生。但由于bash
解析器处理换行符的方式,实际使用的语法(大致)是:
pipeline: command '|' optional_newlines pipeline
结果是optional_newlines
every 后面有一个语法符号|
,因此每个管道占用三个堆栈槽。因此,内存不足错误是在 3,333 个管道符号之后生成的。
yacc 解析器检测并发出堆栈溢出信号,它通过调用 来发出信号yyerror("memory exhausted")
。但是,bash
实现会yyerror
丢弃所提供的错误消息,并替换为“在意外标记附近检测到语法错误...”之类的消息。在这种情况下,这有点令人困惑。
笔记
使用运算符最容易观察到关联性的差异
|&
,该运算符通过管道传输 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