Ubuntu Laços
Um laço (em inglês, loop) repete comandos. É ele que transforma "renomear uma foto" em "renomear as 300 fotos da pasta". O Bash tem três laços: for, while e until.
- A estrutura do
for, parte por parte, e cinco jeitos de montar a lista que ele percorre. - Como percorrer arquivos com nomes que têm espaço sem quebrar nada.
- Quando usar
whileeuntil, e como ler um arquivo linha por linha. - Como interromper (
break), pular (continue) e escapar de um laço infinito.
Antes, leia Variáveis e Argumentos e Condicionais. Os exemplos rodam na máquina fictícia notebook, com a usuária ana.
A ideia: "para cada item, faça"
Pense numa lista de compras. Você pega o primeiro item, coloca no carrinho, pega o próximo, coloca no carrinho… até a lista acabar. O for faz isso com qualquer lista de palavras.
for VARIÁVEL in LISTA; do comandos que usam $VARIÁVEL done
- for fruta
- "Para cada…": cria a variável
fruta, que vai receber um item por vez. - in banana maçã uva
- A lista. Cada palavra separada por espaço é um item.
- ; do
- "Faça": começa o bloco que será repetido.
- echo "Eu gosto de $fruta"
- O comando repetido.
$frutavale "banana", depois "maçã", depois "uva". - ; done
- "Feito": fim do bloco.
$ for fruta in banana maçã uva; do echo "Eu gosto de $fruta"; done Eu gosto de banana « 1ª volta: fruta=banana Eu gosto de maçã « 2ª volta: fruta=maçã Eu gosto de uva « 3ª volta: fruta=uva
Cada repetição se chama volta ou iteração. Quando a lista acaba, o Bash segue para o que vem depois do done.
Em scripts, escrevemos o mesmo laço em várias linhas, com o bloco recuado:
#!/usr/bin/env bash for fruta in banana maçã uva; do echo "Eu gosto de $fruta" done
Cinco formas de montar a lista do for
1. Palavras escritas à mão
$ for servidor in web1 web2 banco; do echo "Verificando $servidor"; done Verificando web1 Verificando web2 Verificando banco
2. Sequência com chaves {1..5}
As chaves geram a lista antes do laço começar (veja touch e mkdir):
$ for i in {1..5}; do echo "Volta $i"; done
Volta 1
Volta 2
Volta 3
Volta 4
Volta 5
3. Arquivos com curinga *
É o uso mais comum. O *.jpg vira a lista de arquivos que terminam em .jpg (veja Curingas):
$ ls fotos festa.jpg 'minha foto.jpg' praia.jpg serra.jpg $ for f in fotos/*.jpg; do echo "Achei: $f"; done Achei: fotos/festa.jpg Achei: fotos/minha foto.jpg « o espaço não atrapalhou Achei: fotos/praia.jpg Achei: fotos/serra.jpg
4. A saída de um comando $(…)
$ for n in $(seq 1 3); do echo "linha $n"; done linha 1 linha 2 linha 3
Cuidado: $(…) quebra a saída em cada espaço. Funciona bem com números e palavras simples, mas não com nomes de arquivo. Veja "Quando não funciona", mais abaixo.
5. Estilo C: contador com início, condição e passo
- for ((
- Abre o laço em estilo C (parênteses duplos).
- i = 0;
- Início: a variável começa em 0.
- i < 10;
- Condição: continua enquanto
ifor menor que 10. - i += 3
- Passo: soma 3 depois de cada volta.
- )); do … done
- Fecha os parênteses e define o bloco.
$ for ((i = 0; i < 10; i += 3)); do echo "i vale $i"; done i vale 0 i vale 3 i vale 6 i vale 9 « 12 não é menor que 10: o laço para
Missão: use um for para criar os arquivos nota1.txt até nota5.txt. Dica: for i in {1..5}; do touch "nota$i.txt"; done. Depois rode for f in *.txt; do echo "arquivo: $f"; done e veja o laço percorrer os arquivos que você criou.
Exemplo prático: renomear várias fotos
A câmera salvou as fotos como .jpeg e você quer .jpg. Uma por uma, seriam dezenas de mv. Com um laço, é um script de 4 linhas:
#!/usr/bin/env bash
# Troca a extensão .jpeg por .jpg em todos os arquivos da pasta fotos
for arquivo in fotos/*.jpeg; do
novo="${arquivo%.jpeg}.jpg"
mv -v "$arquivo" "$novo"
done
- for arquivo in fotos/*.jpeg
- Percorre cada arquivo .jpeg da pasta.
- novo="${arquivo%.jpeg}.jpg"
${arquivo%.jpeg}é o nome sem o final ".jpeg"; depois colamos ".jpg".- mv -v "$arquivo" "$novo"
- Renomeia e mostra o que fez. As aspas protegem nomes com espaço.
$ ./renomear.sh renomeado 'fotos/festa.jpeg' -> 'fotos/festa.jpg' renomeado 'fotos/minha foto.jpeg' -> 'fotos/minha foto.jpg' renomeado 'fotos/praia.jpeg' -> 'fotos/praia.jpg' renomeado 'fotos/serra.jpeg' -> 'fotos/serra.jpg'
Troque o mv -v por echo mv na primeira vez. O laço só mostra o que faria, sem mexer em nada. Se a lista estiver certa, volte o mv.
while: repetir enquanto for verdade
O while ("enquanto") não percorre uma lista: ele repete enquanto um comando ou teste der certo. Use quando você não sabe de antemão quantas voltas serão.
while CONDIÇÃO; do comandos done
#!/usr/bin/env bash n=3 while [ "$n" -gt 0 ]; do echo "Faltam $n..." n=$((n - 1)) sleep 1 done echo "Pronto!"
- while [ "$n" -gt 0 ]
- Enquanto
nfor maior que 0… - n=$((n - 1))
- …diminui 1. Sem essa linha, o laço nunca terminaria.
- sleep 1
- Espera 1 segundo entre as voltas.
$ ./contagem.sh Faltam 3... Faltam 2... Faltam 1... Pronto!
Ler um arquivo linha por linha
Este é o uso mais importante do while. O read lê uma linha por vez, e o laço acaba quando o arquivo termina.
#!/usr/bin/env bash numero=1 while IFS= read -r linha; do echo "$numero: $linha" numero=$((numero + 1)) done < frutas.txt
- IFS=
- Não corta os espaços do começo e do fim da linha.
- read -r linha
- Lê uma linha para a variável
linha. O-rnão trata a barra\como especial. - done < frutas.txt
- O arquivo entra no laço inteiro pelo
<(veja Redirecionamento).
$ cat frutas.txt banana maçã laranja $ ./ler.sh 1: banana 2: maçã 3: laranja
until: repetir até ficar verdade
until ("até que") é o contrário do while: repete enquanto a condição for falsa.
$ n=1; until [ $n -gt 3 ]; do echo "n=$n"; n=$((n+1)); done n=1 n=2 n=3
Um uso real: until ping -c 1 192.168.0.1 > /dev/null; do sleep 5; done; echo "A rede voltou" espera a rede voltar.
Controlar o laço: break e continue
| Comando | O que faz |
|---|---|
break | Sai do laço na hora ("quebra" o laço). |
continue | Pula o resto desta volta e vai para a próxima. |
$ for i in {1..10}; do if [ $i -eq 4 ]; then break; fi; echo $i; done
1
2
3 « no 4, o break encerrou o laço
$ for i in {1..5}; do if [ $i -eq 3 ]; then continue; fi; echo $i; done
1
2
4 « o 3 foi pulado
5
Laço infinito (de propósito)
while true nunca termina sozinho. É útil para monitorar algo de tempos em tempos. Para parar, aperte Ctrl+C.
$ while true; do date +%T; sleep 2; done 10:30:00 10:30:02 10:30:04 ^C « Ctrl+C interrompe
Formas diferentes de processar vários arquivos
Tarefa: mostrar o nome de cada .jpg, inclusive em subpastas, sem quebrar nomes com espaço.
| Forma | Exemplo | Melhor quando… |
|---|---|---|
| for com curinga | for f in fotos/*.jpg; do …; done | Os arquivos estão numa pasta só. É a forma mais simples e segura. |
for com ** | shopt -s globstar; for f in **/*.jpg; do …; done | Incluir subpastas, ainda com for. |
| find + while read | find . -name "*.jpg" -print0 | while IFS= read -r -d "" f; do …; done | Muitas pastas e filtros do find (data, tamanho). |
| find -exec | find . -name "*.jpg" -exec echo {} \; | Um comando só por arquivo, sem laço (veja find). |
| xargs | find . -name "*.jpg" -print0 | xargs -0 ls -l | Passar muitos arquivos de uma vez a um comando (veja Pipes e xargs). |
$ find fotos -name "*.jpg" -print0 | while IFS= read -r -d "" f; do echo "-> $f"; done -> fotos/serra.jpg -> fotos/praia.jpg -> fotos/minha foto.jpg -> fotos/festa.jpg
O find não ordena: a ordem depende de como os arquivos estão gravados no disco.
Quando não funciona
$ for f in $(ls fotos); do echo "[$f]"; done [festa.jpg] [minha] « "minha foto.jpg" foi partido [foto.jpg] [praia.jpg] [serra.jpg]
O que aconteceu: $(ls …) devolve um texto, e o Bash corta esse texto em cada espaço.
Como resolver: nunca use $(ls) num laço. Use o curinga direto: for f in fotos/*; do echo "[$f]"; done. E sempre "$f" entre aspas dentro do laço.
$ for f in *.pdf; do echo "Achei: $f"; done Achei: *.pdf
O que aconteceu: não havia nenhum PDF. Quando o curinga não encontra nada, o Bash entrega o texto *.pdf como está, e o laço roda uma vez com ele.
Como resolver: no começo do script, ative shopt -s nullglob (curinga sem resultado vira lista vazia). Outra opção é testar dentro do laço: [ -e "$f" ] || continue.
$ printf "a\nb" > semfim.txt $ while read -r l; do echo "[$l]"; done < semfim.txt [a]
O que aconteceu: a última linha não termina com "Enter" (quebra de linha). O read leu o "b", mas devolveu código 1, e o while parou antes de usá-lo. Acontece com arquivos criados no Windows ou em alguns editores.
Como resolver: while read -r l || [ -n "$l" ]; do …; done < arquivo processa também essa última linha.
$ cat frutas.txt | while read -r f; do total=$((total+1)); done; echo "total=$total" total=
O que aconteceu: com |, o while roda num processo separado (um "subshell"). O total que ele contou some quando o laço termina.
Como resolver: mande o arquivo com < em vez de cat |:
$ total=0; while read -r f; do total=$((total+1)); done < frutas.txt; echo "total=$total" total=3
$ for i in 1 2 3 do echo $i; done bash: erro de sintaxe próximo ao token inesperado `done'
O que aconteceu: faltou o ; antes do do. O Bash achou que "do" e "echo" eram mais itens da lista.
Como resolver: for i in 1 2 3; do echo $i; done.
O que aconteceu: num while, a condição nunca fica falsa, normalmente porque você esqueceu de mudar a variável (o n=$((n - 1)) do exemplo).
Como resolver: Ctrl+C interrompe. Se o laço estiver rodando em segundo plano, encontre e encerre o processo (veja Encerrar Programas).
Referência rápida
| Estrutura | Sintaxe |
|---|---|
| for em lista | for x in a b c; do …; done |
| for em sequência | for i in {1..10}; do …; done |
| for em arquivos | for f in *.txt; do …; done |
| for estilo C | for ((i=0; i<10; i++)); do …; done |
| while | while COND; do …; done |
| Ler linhas | while IFS= read -r l; do …; done < arq |
| until | until COND; do …; done |
| Infinito | while true; do …; done |
| Sair / pular | break / continue |
| Curinga vazio | shopt -s nullglob |
Exercícios
1. Completar o for
Complete para mostrar os números de 1 a 10:
2. Palavras que faltam
Complete as duas palavras-chave do laço:
3. Ler linha por linha
Complete para ler lista.txt linha por linha:
4. Por que for f in $(ls) é perigoso?
5. O que o continue faz dentro de um laço?
Resumo
forpercorre uma lista: palavras,{1..10}, arquivos com*ou a saída de$(…).- Para arquivos, use o curinga direto e
"$f"entre aspas; nunca$(ls). whilerepete enquanto der certo;while IFS= read -r l … < arquivolê linha por linha.untilrepete até dar certo;breaksai,continuepula.- Ctrl+C interrompe um laço que não para.