eval limitação com comandos canalizados

4

Temos um script de shell que cria uma cadeia de comandos longa canalizada em uma variável e a executa com eval (o código a seguir é simplificado para o essencial):

 cmd="cat /some/files | grep -v \"this\" | grep -v \"that\""
 cmd="$cmd | grep -v \"much more dynamical filter with variables\""
 ...
 result='eval $cmd'

Tudo funcionou bem até agora, mas agora parece que o conteúdo da variável cmd excede um limite. Quando exceder cerca de 95970 bytes, receberei o erro (embora a sintaxe esteja correta):

eval: line ...: syntax error near unexpected token '|'

Eu fiz alguma pesquisa, mas não tive a menor idéia (getconf ARG_MAX ecoa 2621440, ulimit -a não me ajudou também).

Alguém poderia, por favor, explicar qual limite poderia ser e, talvez, como aumentar o limite ou qual é a melhor maneira de evitá-lo?

EDIT: Eu testei agora em três servidores diferentes (centos) com um script designado. Em todos os servidores acabei atingindo 3333 pipes em um comando com eval.

E eu encontrei outro página onde alguém experimentou o mesmo mas sem eval. Então parece ser apenas um limite de tubos.

Saber que a limitação é provavelmente causada pelo número de canos me ajudará a solucionar o problema. Então, essa não é mais a questão.

Mas ainda estou interessado em saber como esse limite é definido ou pelo menos como detectar o valor do limite (provavelmente não em todos os sistemas 3333) sem executar um script para isso.

Pode ser reproduzido com:

yes cat | head -n 3334 | paste -sd '|' - | bash
    
por hellcode 20.02.2015 / 14:43

1 resposta

9

O problema aqui é, na verdade, um problema com o bash parser. Não há alternativa além de editar e recompilar bash , e o limite 3333 provavelmente será o mesmo em todas as plataformas.

O analisador bash é gerado com yacc (ou, normalmente, com bison , mas no modo yacc ). yacc analisadores são analisadores bottom-up, usando o algoritmo LALR (1) que cria uma máquina de estados finitos com uma pilha de empilhamento. Falando livremente, a pilha contém todos os símbolos ainda não reduzidos, juntamente com informações suficientes para decidir quais produções usar para reduzir os símbolos.

Esses analisadores são otimizados para regras gramaticais recursivas à esquerda. No contexto de uma gramática de expressão, uma regra recursiva à esquerda se aplica a um operador associativo à esquerda, como um - b em matemática comum. Isso é deixado associativo porque a expressão um - b - c grupos ("associados") para a esquerda, tornando-o igual a ( a - b ) - c em vez de a - ( b - c ). Por contraste, a exponenciação é associativa à direita, de modo que a b c é por convenção avaliada como a ( b c ) em vez de ( a b ) c .

bash operadores são operadores de processo, em vez de operadores aritméticos; estes incluem booleans de curto-circuito ( && e || ) e pipes ( | e |& ), bem como operadores de sequenciamento ; e & . Como os operadores matemáticos, a maioria deles associa-se à esquerda, mas os operadores de pipe associam-se à direita, de modo que cmd1 | cmd2 | cmd3 seja analisado como se fosse cmd1 | { cmd2 | cmd3 ; } , em oposição a { cmd1 | cmd2 ; } | cmd3 . (Na maioria das vezes a diferença não é importante, mas é observável. [Veja Nota 1])

Para analisar uma expressão que é uma sequência de operadores associativos à esquerda, você precisa apenas de uma pequena pilha de analisadores. Toda vez que você apertar um operador, você pode reduzir (parênteses, se quiser) a expressão à esquerda dele. Por outro lado, analisar uma expressão que é uma sequência de operadores associativos à direita exige que você coloque todos os símbolos na pilha do analisador até chegar ao final da expressão, porque somente então você pode começar a reduzir (inserindo parênteses). (Essa explicação envolve um pouco de aceno de mão, uma vez que foi planejada para ser não-técnica, mas é baseada no funcionamento do algoritmo real.)

Os analisadores Yacc redimensionarão a pilha do analisador no tempo de execução, mas há um tamanho máximo de pilha em tempo de compilação, que por padrão é de 10.000 slots. Se a pilha atingir o tamanho máximo, qualquer tentativa de expansão causará um erro de falta de memória. Porque | é associativo à direita, uma expressão da forma:

statement | statement | ... | statement 

acabará por desencadear este erro. Se fosse analisado da maneira óbvia, isso aconteceria depois de 5.000 símbolos de pipe (com 5.000 instruções). Mas devido à maneira como o bash analisador manipula as novas linhas, a gramática real usada é (aproximadamente):

pipeline: command '|' optional_newlines pipeline

com a conseqüência de que há um símbolo de% gramática optional_newlines após cada | , então cada canal ocupa três slots de pilha. Portanto, o erro de falta de memória é gerado após 3.333 símbolos de pipe.

O analisador yacc detecta e sinaliza o estouro de pilha, que ele chama chamando yyerror("memory exhausted") . No entanto, a implementação bash de yyerror elimina a mensagem de erro fornecida e substitui uma mensagem como "erro de sintaxe detectado próximo ao token inesperado ...". Isso é um pouco confuso neste caso.

Notas

  1. A diferença na associatividade é mais facilmente observada usando o operador |& , que canaliza stderr e stdout. (Ou, mais precisamente, duplica stdout em stderr depois de estabelecer o pipe.) Para um exemplo simples, suponha que o arquivo foo não exista no diretório atual. Então

    # 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
    
por 23.02.2015 / 22:52

Tags