Desenvolvimento

19 jul, 2016

Bash for Loop, o primeiro passo na automação no Linux

Publicidade

Acredito que dominar o loop for do Bash em Linux é um dos fundamentos para administradores de sistemas Linux (e até mesmo para os desenvolvedores!) que levam suas habilidades de automação para o próximo nível. Neste artigo, vou explicar como eles funcionam e oferecer alguns exemplos úteis.

Deixa eu começar dizendo algo embaraçoso. Durante os primeiros quatro ou cinco anos da minha carreira em Linux – que está se aproximando de 10 anos de experiência profissional -, eu nunca usei loops em scripts Bash. Ou na linha de comando.

Era o seguinte: eu era um clicador de mouse muito rápido. E um copiador/colador muito rápido. E alguém bom de pesquisar e substituir no vim e em outros editores de texto. Muitas vezes, isso me levou a uma solução de trabalho mais rápida do que trabalhar com a sintaxe peculiar, testes, correções de bugs, … de loops no Bash.

E, para ser completamente honesto, se você está administrando apenas um par de servidores, acho que você pode se safar de não usar loops no Bash. Mas, depois de dominar isso, você vai pensar: por que não aprendeu loop para o Bash mais cedo?

Bash for Loop: exemplo

Primeiro, deixa eu mostrar a forma mais básica – e aquele que você verá com mais frequência — de um loop para o Bash.

#!/bin/bash
for i in 1 2 3 4 5; do
  echo "counter: $i"
done

Se você executar esse script, ele exibe algo assim:

$ ./script.sh
counter: 1
counter: 2
counter: 3
counter: 4
counter: 5

Bastante básico, certo? Aqui está como ele é dividido.

bash_1

A primeira parte, #!/bin/bash, é a shebang, algumas vezes chamada de hashbang. Ela indica que intérprete vai ser usado para analisar o resto do script. Em suma, é o que torna esse um script Bash.

O resto é onde o loop do Bash realmente entra.

  1. for: indica que este é um loop, e que você gostaria de fazer uma iteração (ou “passar por cima”) de vários itens.
  2. i: um lugar reservado para uma variável, que pode mais tarde ser referenciado como $i. i é frequentemente usado por desenvolvedores para loop ou iteração sobre um array ou um hash, mas isso pode ser qualquer coisa (*). Para maior clareza, ele também poderia ter sido nomeado counter, a variável para fazer referência a ele mais tarde seria então chamada de $counter, com um sinal de dólar.
  3. in: uma palavra-chave, indicando o separador entre a variável i e a coleção de itens que devem ser percorridos (iterados).
  4. 1 2 3 4 5: o que ocorrer entre a palavra-chave in e o delimitador ; é a coleção de itens que você deseja percorrer. Neste exemplo, o conjunto “1 2 3 4 5” é considerado um conjunto de cinco itens individuais.
  5. do: esta palavra-chave define que, a partir deste ponto, o loop começa. O código que se segue será executado n vezes, onde n é a quantidade de itens que está na coleção, neste caso, um conjunto de cinco dígitos.
  6. echo “counter: $i”: este é o código dentro do loop, que será repetido – neste caso – cinco vezes. A variável $i é o valor individual de cada item.
  7. done: esta palavra-chave indica que o código que deve ser repetido neste loop terminou.

(*) Tecnicamente, a variável não pode ser qualquer coisa, pois há limitações de caracteres que podem ser usados em uma variável, mas isso está além do escopo do que estamos discutindo aqui. Mantenha como alfanuméricos sem espaços ou caracteres especiais, e você provavelmente estará seguro.

Um monte de texto, não é mesmo?

Bom, olhe para a imagem novamente e lembre-se das diferentes partes do loop. E lembre também que isso não está limitado a “scripts”, ele pode ser concatenado para uma única linha para o uso na linha de comando, também.

$ for i in 1 2 3 4 5; do echo "counter: $i"; done

A mesma divisão ocorre lá.

bash_2

Há uma diferença importante. Logo antes da última palavra-chave done, há um ponto e vírgula ; para indicar que o comando termina lá. No script Bash, isso não é necessário, porque a palavra-chave done é colocada em uma nova linha, também terminando o comando acima.

Na verdade, esse primeiro exemplo que eu mostrei pode ser reescrito sem um único ;, apenas colocando uma nova-alinhando para cada linha.

#!/bin/bash
for i in 1 2 3 4 5
do
  echo "counter: $i"
done

Você pode escolher qual estilo você preferir ou achar mais legível/fácil de manter.

O resultado é exatamente o mesmo: um conjunto de itens é iterado e para cada ocorrência uma ação é tomada.

Valores para o loop no Bash

Looping com variáveis não é muito emocionante em si, mas se torna muito útil quando você começar a experimentar com os dados.

Por exemplo:

$ for file in *; do echo "$file"; done
file1.txt
file2.txt
file3.txt

Você só pode fazer ls e obter o mesmo valor, mas você pode usar * dentro de sua instrução for, que é essencialmente o mesmo que um $(ls), mas com a saída segura. É executado antes do for-loop e a saída está sendo usada como a coleção de itens para iterar.

Isso abre uma série de oportunidades, especialmente se você tiver o comando seq em mente. Com seq, você pode gerar sequências no CLI.

Por exemplo:

$ seq 25 30
25
26
27
28
29
30

Isso gera os números de 25 a 30. Então, se você quiser um loop de itens do 1 até o 255, você pode fazer assim:

$ for counter in $(seq 1 255); do echo "$counter"; done

Quando você usaria isso? Talvez para executar ping em um par de IPs ou se conectar a múltiplos hosts remotos e disparar alguns comandos.

$ for counter in $(seq 1 255); do ping -c 1 "10.0.0.$counter"; done
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
...

Agora estamos conversando.

Intervalos no Bash

Você também pode usar alguns dos primitivos internos do Bash para gerar um intervalo (range), sem o uso de seq. O código a seguir faz exatamente o mesmo que o exemplo do ping acima.

$ for counter in {1..255}; do ping -c 1 10.0.0.$counter; done

As versões mais recentes do Bash (Bash 4.x pelo menos) também podem modificar esse comando, aumentando o incremento de cada inteiro. Por padrão, é sempre +1, mas você pode fazer +5 se quiser.

$ for counter in {1..255..5}; do echo "ping -c 1 10.0.0.$counter"; done
ping -c 1 10.0.0.1
ping -c 1 10.0.0.6
ping -c 1 10.0.0.11
ping -c 1 10.0.0.16

Itens de looping podem permitir que você automatize rapidamente uma tarefa de outra forma mundana.

Encadeamento de vários comandos no loop for do Bash

Você, obviamente, não está limitado a um único comando em um loop for, você pode encadear vários que estão no interior do loop for.

#!/bin/bash
for i in 1 2 3 4 5; do
  echo "Hold on, connecting to 10.0.1.$i"
  ssh root@"10.0.1.$i" uptime
  echo "All done, on to the next host!"
done

Ou, na linha de comando como um one-liner (uma linha):

$ for i in 1 2 3 4 5; do echo "Hold on, connecting to 10.0.1.$i"; ssh root@"10.0.1.$i" uptime; echo "All done, on to the next host"; done

Você pode encadear vários comandos com o ponto e vírgula ;, o último comando será a palavra-chave done, para indicar que você terminou.

Exemplos de loop for no Bash

Aqui estão alguns exemplos do “loop for para o bash”. Eles não são necessariamente os mais úteis, mas mostram algumas das possibilidades.

Para cada usuário no sistema, escreva o hash de senha dele para um arquivo nomeado com o nome dele

Uma linha:

$ for username in $(awk -F: '{print $1}' /etc/passwd); do grep $username /etc/shadow | awk -F: '{print $2}' > $username.txt; done

Script:

#!/bin/bash
for username in $(awk -F: '{print $1}' /etc/passwd)
do
  grep $username /etc/shadow | awk -F: '{print $2}' > $username.txt
done

Renomeie todos os arquivos *.txt para remover a extensão de arquivo

Uma linha:

$ for filename in *.txt; do mv "$filename" "${filename%.txt}"; done

Script:

!#/bin/bash
for filename in *.txt
do
  mv "$filename" "${filename%.txt}"
done

Use cada linha em um arquivo como um IP para conexão

Uma linha:

$ for ip in $(cat ips.txt); do ssh root@"$ip" yum -y update; done

Script:

#!/bin/bash
for ip in $(cat ips.txt)
do
  ssh root@"$ip" yum -y update
done

Depuração de loops no Bash

Aqui está uma maneira que eu realmente gosto para depurar loops for: apenas uso echo em tudo. Essa também é uma ótima maneira de “gerar” um script Bash estático, pegando a saída.

Por exemplo, no exemplo de ping, você pode fazer isto:

$ for counter in {1..255..5}; do echo "ping -c 1 10.0.0.$counter"; done

Isso fará um echo em cada declaração do ping. Agora você também pode pegar essa saída, escrever em outro arquivo Bash e manter para o futuro (ou modificar manualmente, se você está lutando com o loop para o Bash – passei por isso, já fiz isso).

$ for counter in {1..255..5}; do echo "ping -c 1 10.0.0.$counter"; done > ping-all-the-things.sh
$ more ping-all-the-things.sh
ping -c 1 10.0.0.1
ping -c 1 10.0.0.6
ping -c 1 10.0.0.11
...

Pode ser primitivo, mas isso faz com que você avance um longo caminho!

***

Mattias Geniar faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://ma.ttias.be/bash-loop-first-step-automation-linux/