Por que não usar backticks com loop?

5

Algum tempo atrás, publiquei uma resposta a algumas perguntas sobre scripts. Alguém apontou que eu não deveria usar o seguinte comando:

for x in $(cat file); do something; done 

mas em vez disso:

while read f; do something; done < file
O artigo

Uso inútil do gato supõe explicar todo o problema, mas a única explicação é:

The backticks are outright dangerous, unless you know the result of the backticks is going to be less than or equal to how long a command line your shell can accept. (Actually, this is a kernel limitation. The constant ARG_MAX in your limits.h should tell you how much your own system can take. POSIX requires ARG_MAX to be at least 4,096 bytes.)

Se eu entendi corretamente, o bash (?) deve travar se eu usar a saída de um arquivo muito grande no comando (ele deve exceder ARG_MAX no arquivo limits.h). Então eu verifiquei ARG_MAX com o comando:

> grep ARG_MAX /usr/src/kernels/$(uname -r)/include/uapi/linux/limits.h
#define ARG_MAX       131072    /* # bytes of args + environ for exec() */

Em seguida, criei um arquivo contendo texto sem espaços:

> ls -l
-rw-r--r--. 1 root root 100000000 Aug 21 15:37 in_file

Então eu corro:

for i in $(cat in_file); do echo $i; done

aaae nada de terrível aconteceu.

Então, o que devo fazer para verificar se / como essa coisa 'não usar gato com laço' é perigosa?

    
por mrc02_kr 21.08.2017 / 15:44

3 respostas

3

Depende do que file deve conter. Se for destinado a conter uma lista separada por IFS de globs de shell como (assumindo o valor padrão de $IFS ):

/var/log/*.log /var/adm/*~
/some/dir/*.txt

Então for i in $(cat file) seria o caminho a percorrer. Como é isso que $(cat file) sem aspas: aplica o operador split + glob na saída de cat file de seus caracteres de nova linha à direita. Por isso, passaria por cima de cada nome de ficheiro resultante das expansões desses globs (excepto nos casos em que os globs não correspondem a nenhum ficheiro em que isso deixaria o glob lá, mas não expandido).

Se você quisesse fazer um loop por cada linha delimitada de file , você faria:

while IFS= read -r line <&3; do
{
  something with "$line"
} 3<&-
done 3< file

Com um loop for , você pode percorrer todas as linhas não vazias com:

IFS='
' # split on newline only (actually sequences of newlines and
  # ignoring leading and trailing ones as newline is a
  # IFS whitespace character)
set -o noglob # disable the glob part of the split+glob operator:
for line in $(cat file); do
   something with "$line"
done

No entanto, um:

while read line; do
  something with "$line"
done < file

Faz pouco sentido. Isso é lendo o conteúdo de file de uma maneira muito complicada , onde caracteres de $IFS e barras invertidas são tratados especialmente.

Em qualquer caso, o limite ARG_MAX ao qual você está citando refere-se à chamada de sistema execve() (no tamanho cumulativo dos argumentos e variáveis de ambiente), portanto, aplica-se somente aos casos em que um comando no sistema de arquivos está sendo executado com a expansão possivelmente muito longa do operador split + glob aplicado à substituição de comando (esse texto é enganoso e errado em várias contas).

Seria aplicável por exemplo em:

cat -- $(cat file) # with shell implementations where cat is not builtin

Mas não em:

for i in $(cat file)

em que não há nenhuma chamada de sistema execve() envolvida.

Compare:

bash-4.4$ echo '/*/*/*/*' > file
bash-4.4$ true $(cat file)
bash-4.4$ n=0; for f in $(cat file); do ((n++)); done; echo "$n"
523696
bash-4.4$ /bin/true $(cat file)
bash: /bin/true: Argument list too long

Tudo bem com o comando bash builtin de true ou com o loop for , mas não ao executar /bin/true . Observe como o file tem apenas 9 bytes, mas a expansão de $(cat file) é de vários megabytes porque o /*/*/*/* glob está sendo expandido pelo shell.

Mais leitura em:

por 21.08.2017 / 17:59
1

@chepner explicou a diferença nos comentários:

for i in $(cat in_file) doesn't iterate over the lines of the file, it iterates over the words resulting from the contents of the file being subjected to word-splitting and pathname expansion.

Para o impacto no desempenho e no uso de recursos, fiz um pequeno benchmark para os dois casos usando a entrada com linhas de 1M (cerca de 19M) e medindo o tempo e o uso de memória com /usr/bin/time -v :

test1.sh:

#!/bin/bash
while read x
do
    echo $x > /dev/null
done < input

Resultados:

Command being timed: "./test1.sh"
User time (seconds): 12.41
System time (seconds): 2.03
Percent of CPU this job got: 110%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:13.07
Maximum resident set size (kbytes): 3088

test2.sh:

#!/bin/bash
for x in $(cat input)
do
    echo $x > /dev/null
done

Resultados:

Command being timed: "./test2.sh"
User time (seconds): 17.19
System time (seconds): 3.13
Percent of CPU this job got: 109%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:18.51
Maximum resident set size (kbytes): 336356

Enviei a saída completa de ambos os testes para o pastebin . Com bash usando for i in $(cat ...) utiliza significativamente mais memória e também roda mais devagar. No entanto, os resultados podem variar dependendo se você executaria esses mesmos testes em algum outro shell.

    
por 21.08.2017 / 16:22
-1

while loops pode ser problemático, principalmente porque eles usam entrada padrão por padrão (portanto ssh -n ), então se você precisar de entrada padrão para outra coisa, um loop while falhará

$ find . -name "*.pm" | while read f; do aspell check $f; done
$ 

não faz nada porque aspell quer um terminal que é ocupado por uma lista de nomes de módulos perl; um loop for é mais adequado (assumindo que os nomes de arquivos não serão divididos pelas regras de divisão de palavras POSIX):

$ for f in $(find . -name \*.pm); do aspell check $f; done
...

como isso não usa entrada padrão como while faz por padrão.

Além disso, while está propenso a perda de dados silenciosa (e for se comporta de maneira diferente para a mesma entrada):

$ echo -n mmm silent data loss | while read line; do echo $line; done
$ for i in $(echo -n mmm silent data loss); do echo $i; done
mmm
silent
data
loss
$ 

Assim, argumentos podem ser feitos de que while é perigoso e não deve ser usado, dependendo do contexto.

    
por 21.08.2017 / 16:29