$ portaldoshell guia grátis

técnica · 7 min de leitura

A linha que vale mais que três loops

Quem escreveu os três for gastou parágrafo. Quem usou chaves gastou uma linha.

$ whoami
Julio Cezar Neves

Num escritório antigo, funcionários de camisa e gravata correm dentro de grandes rodas giratórias de metal carregando caixinhas, como hamsters. Em primeiro plano, um homem grisalho de barba relaxa na cadeira, de olhos fechados, enquanto puxa a alavanca de uma máquina em forma de chaves que despeja centenas de cubos numerados sobre a mesa.

A expansão de chaves resolve em uma linha o que três loops aninhados levam parágrafos para explicar. Mesmo assim, a maior parte de quem programa em Shell passa longe desse recurso, porque aprendeu decorando a sintaxe do for e parou de procurar ali mesmo. O hábito é compreensível. O loop foi a primeira ferramenta que funcionou, e a primeira ferramenta que funciona costuma ganhar estabilidade vitalícia na cabeça de quem a usa.

Dias atrás caiu um problema clássico no grupo de Bash do Dicas-L no Telegram, onde muita gente traz os enigmas do dia a dia e eu fico espiando as soluções. Eram três variáveis para combinar. A primeira ia de 1 a 14 e a segunda de 1 a 8, enquanto a terceira fechava a conta indo de 1 a 64. O objetivo era gerar todas as combinações no formato var1/var2/var3, sem deixar nenhuma de fora.

A resposta que quase todo mundo digita de cabeça, antes mesmo de pensar numa alternativa, é o loop aritmético aninhado:

for ((var1=1; var1<=14; var1++)); do
  for ((var2=1; var2<=8; var2++)); do
    for ((var3=1; var3<=64; var3++)); do
      echo "$var1/$var2/$var3"
    done
  done
done

O mais interno faz a var3 subir de 1 até 64, imprimindo o trio a cada passo. Por fora dele, a var2 decide quantas vezes esse ciclo se repete, e por cima de tudo a var1 empurra o conjunto inteiro, reiniciando os dois loops de dentro a cada incremento. Quando a var3 bate em 64, a var2 avança uma casa e a var3 recomeça do 1. É uma dúzia de linhas para chegar num resultado que cabe inteiro numa única instrução.

Funciona, e ninguém discute isso. O detalhe é que funcionar e ser a solução mais enxuta raramente moram no mesmo código. Quem já apanhou o bastante de Shell reconhece a diferença entre resolver e resolver bem, e a segunda opção costuma pedir menos linhas. Neste caso, pede uma só:

echo {1..14}/{1..8}/{1..64}

O Shell expande cada chave e cruza as três listas, devolvendo exatamente a mesma sequência dos loops aninhados, as 7.168 combinações. A linha dispensa variável e dispensa contador. O interpretador faz sozinho o trabalho que a pessoa insistia em fazer à mão, e termina antes de alguém perceber que ele começou. Para ter uma combinação por linha, como o loop entregava, basta trocar o echo por printf '%s\n' {1..14}/{1..8}/{1..64}.

A hierarquia é a mesma dos três loops. A chave da direita varia mais depressa e a da esquerda mais devagar, tudo resolvido pelo próprio parser da linha de comando num único disparo. Com listas pequenas, o mecanismo fica inteiro à vista:

$ echo {1..2}/{1..2}/{1..3}
1/1/1 1/1/2 1/1/3 1/2/1 1/2/2 1/2/3 2/1/1 2/1/2 2/1/3 2/2/1 2/2/2 2/2/3

Quem escreveu os três for gastou parágrafo. Quem usou chaves gastou uma linha e ainda sobrou tempo no meio do expediente. Um caso isolado sempre soa como coincidência, e coincidência convence pouco quem já apanhou de produção. Imagine, então, numerar de 00 a 99, com zero à esquerda nos números de um dígito. A primeira ideia de quem usa Shell há algum tempo é o seq, com o -w cuidando do preenchimento:

seq -w 0 99

A mesma numeração nasce de outro jeito, com o zero já embutido na própria chave:

echo {00..99}

O resultado na tela é idêntico ao do seq -w, com um aviso importante. O preenchimento com zeros dentro da chave chegou no Bash 4, e o Bash 3.2, que o macOS ainda entrega por padrão, devolve 0 1 2 sem zero algum. Quem testa no Mac pode concluir que a dica está errada. Antes de culpar a dica, vale um echo $BASH_VERSION, que costuma encerrar a discussão em poucos segundos.

A diferença de verdade aparece no relógio. Rodei os dois métodos 2.000 vezes cada, com a saída jogada no /dev/null, e no Bash 3.2 da máquina usei {0..99}, que gera a mesma quantidade de números. O seq -w 0 99 levou 1,63 segundo, e a expansão de chaves levou 0,10, cerca de dezesseis vezes menos. Os números mudam de máquina para máquina, e a proporção costuma se manter.

A explicação está na origem de cada um. O seq é um programa externo, e cada chamada obriga o Shell a criar um processo novo, carregar o binário e esperar a resposta. É trabalho de fork e de exec, um custo pequeno que se acumula a cada volta do loop até virar conta. Dentro de um script que roda milhares de vezes por dia, esse custo pequeno ganha cadeira cativa no relatório de desempenho.

A expansão de chaves dispensa tudo isso. Ela acontece dentro do próprio interpretador e é a primeira das expansões que o Bash faz ao ler a linha, antes da expansão de variáveis e dos coringas de nome de arquivo. Nenhum processo filho nasce, e o parser resolve a tarefa mais depressa do que qualquer programa auxiliar conseguiria. A velocidade vem de graça, embutida num recurso que muita gente atravessa a carreira sem usar.

A ironia é que o recurso mais rápido é também o mais esquecido. Muita gente decora seq e printf para gerar e formatar número, e ignora que a resposta mais econômica estava embutida no Shell desde sempre, sem instalar nada. Programador tem o hábito de procurar solução complicada para problema que o terminal resolveu há décadas. O terminal, educado, espera a vez sem reclamar.

Recurso desconhecido continua útil, e apenas espera alguém perguntar por ele. A expansão de chaves está documentada no man do Bash e disponível em qualquer Shell decente, aguardando que alguém troque o reflexo automático do for pela pergunta simples sobre um jeito mais direto. Quem faz essa pergunta antes de sair digitando termina o dia com menos código e mais tempo livre.

O ganho vai além da velocidade. Um loop triplo obriga quem lê o código depois a reconstruir na cabeça a hierarquia das variáveis para entender o que sai dali. Uma linha com três chaves entrega a mesma informação de forma direta, e qualquer pessoa olhando para {1..14}/{1..8}/{1..64} enxerga os limites de cada faixa sem rastrear incremento algum. Código que se explica sozinho é a documentação mais barata que existe.

Esse detalhe separa quem usa Shell de quem domina Shell. A sintaxe básica se aprende em duas semanas. O instinto de enxergar quando uma chave substitui três loops, ou quando um seq esconde uma alternativa mais rápida, vem de prática e de apanhar até o atalho virar reflexo. É o treino de olhar para um loop de três níveis e perguntar, quase sem pensar, se ele precisa mesmo existir.

PS: Rode echo {1..14}/{1..8}/{1..64} no seu terminal antes de comentar, e conte nos comentários qual loop do seu repositório merece virar uma linha. Compartilhe o texto com quem ainda escreve três for para gerar uma lista. Se o resultado inundar a tela, parabéns: são 7.168 combinações, e todas apareceram sem um único contador.

$ tee # compartilhe este artigo WhatsApp Telegram LinkedIn

$ cat comentarios.log

lendo o log…

$ ./comentar.sh --artigo a-linha-que-vale-mais-que-tres-loops

* obrigatório. Todo comentário passa pelo root antes de entrar no log, e o root daqui é humano.