Objetivos de Aprendizagem
Ao final desta aula, o aluno será capaz de:
- Declarar e manipular variáveis, arrays e variáveis de ambiente
- Processar argumentos posicionais e opções com
getopts - Usar condicionais
if/elif/elseecasecom testes robustos - Implementar loops
for,whileeuntil - Criar funções reutilizáveis com escopo de variáveis controlado
- Encadear comandos com pipes e redirecionar fluxos I/O
- Escrever scripts reais: backup automatizado, limpeza de logs, relatório de uso
- Depurar scripts com
set -x,bash -neshellcheck
Pré-Requisitos
Conhecimentos prévios
- Aula 01 concluída — navegação no shell, FHS, permissões básicas
- Saber criar e editar arquivos de texto no terminal (
nano,vimoucat) - Entender pipes (
|) e redirecionamento básico (>,>>)
Ambiente necessário
- VM com Ubuntu 22.04 LTS ou Debian 12 com Bash 5+
shellcheckinstalado:sudo apt install shellcheck- Diretório de trabalho criado:
mkdir -p ~/scripts-aula2 - Editor preferido disponível no PATH
- Este módulo roda 100% dentro do container — shell scripting não depende de nenhuma capacidade privilegiada.
Variáveis de shell
Variáveis armazenam valores temporários dentro do script ou da sessão shell. Não há declaração de tipo — o shell trata tudo como texto a menos que se use aritmética explícita. Por convenção, variáveis locais ficam em minúsculas; variáveis de ambiente em maiúsculas.
# Atribuição: sem espaços ao redor do '=' nome="Alice" versao="2.1" contador=0 # Leitura: prefixo $ echo "Olá, $nome" echo "Versão: ${versao}" # chaves explícitas — recomendado # Variável não definida → string vazia (perigoso sem set -u) echo "Indefinida: ${nao_existe:-valor_padrao}" Indefinida: valor_padrao # Variável somente-leitura readonly MAX_RETRIES=3 # Remover variável unset contador
Variáveis de ambiente
# Exportar para processos filhos export APP_ENV="production" # Ver todas as variáveis de ambiente $ env | grep APP_ENV APP_ENV=production # Variáveis de ambiente importantes echo $HOME # /home/usuario echo $PATH # diretórios de busca de executáveis echo $SHELL # shell atual echo $USER # nome do usuário echo $PWD # diretório atual echo $OLDPWD # diretório anterior
Expansões de variável
# Valor padrão se não definida (não atribui) echo "${HOST:-localhost}" # Atribuir padrão se não definida : "${LOG_DIR:=/var/log/myapp}" # Erro se não definida echo "${REQUIRED_VAR:?'variável obrigatória não definida'}" # Comprimento da string texto="Linux" echo "${#texto}" # → 5 # Substituição de substring arquivo="relatorio.txt" echo "${arquivo%.txt}" # → relatorio (remove sufixo) echo "${arquivo#rela}" # → torio.txt (remove prefixo) # Maiúsculas / minúsculas (Bash 4+) s="hello world" echo "${s^^}" # → HELLO WORLD echo "${s^}" # → Hello world
${var:-x} apenas usa
x como padrão, mas não altera a variável;
${var:=x} atribui x a var se estiver vazia
(por isso costuma vir precedido do no-op :); e ${var:?msg}
aborta o script com msg se a variável não existir — ideal
para tornar argumentos obrigatórios. O : extra faz a expansão disparar
também quando a variável está definida mas vazia; sem ele, só quando está
não definida.
A ordem das expansões do Bash
Antes de executar qualquer comando, o Bash reescreve a linha percorrendo uma sequência
fixa de expansões. Entender essa ordem explica por que
echo ~/*.txt funciona mas echo "$var"* às vezes surpreende:
{a,b,c} e {1..5} geram as sequências (é puramente textual, ocorre primeiro)~ e ~usuario viram caminhos de home$var, ${var:-x}, ${#var}$(...) é executado e substituído pela saída$(( ... )) é calculadoIFS (só fora de aspas!)*, ?, [...] viram nomes reais que existem no discorm $arquivo (sem aspas) quebra em várias
palavras se o nome tiver espaço — enquanto rm "$arquivo" preserva o valor
exato. Regra de ouro: sempre cite "$var", exceto quando você
deliberadamente quer que o valor seja dividido.
Substituição de comando e aritmética
# Substituição de comando: $() ou backticks (prefira $()) hoje="$(date +%Y-%m-%d)" kernel="$(uname -r)" num_procs="$(nproc)" # Aritmética: $(( )) ou let a=10; b=3 echo $(( a + b )) # → 13 echo $(( a * b )) # → 30 echo $(( a % b )) # → 1 (módulo) (( contador++ )) # incremento # Arrays frutas=("maçã" "banana" "laranja") echo "${frutas[0]}" # → maçã echo "${frutas[@]}" # → todos os elementos echo "${#frutas[@]}" # → 3 (tamanho) frutas+=("uva") # append
Arrays: indexados, associativos e fatiamento
O Bash tem dois tipos de array: indexados (chave inteira, o padrão) e
associativos (chave string, exigem declare -A, Bash 4+).
Sempre expanda com "${arr[@]}" entre aspas — assim cada elemento vira uma
palavra, mesmo os que contêm espaços.
$ servidores=("web 01" "web02" "db01") $ echo "${#servidores[@]}" # quantidade 3 $ echo "${servidores[@]:1:2}" # fatia: 2 itens a partir do índice 1 web02 db01 $ echo "${!servidores[@]}" # os índices (chaves) 0 1 2 # Array associativo (mapa chave→valor) $ declare -A porta $ porta[http]=80; porta[https]=443; porta[ssh]=22 $ echo "HTTPS usa a porta ${porta[https]}" HTTPS usa a porta 443 $ for s in "${!porta[@]}"; do echo "$s → ${porta[$s]}"; done ssh → 22 http → 80 https → 443
"${arr[@]}" e "${arr[*]}"
é a mesma que entre "$@" e "$*". Com @, cada elemento é
uma palavra separada — o que você quase sempre quer em loops. Com *, tudo vira
uma única string unida pelo primeiro caractere do IFS.
Variáveis especiais do shell
| Variável | Significado | Exemplo de uso |
|---|---|---|
$0 | Nome do script | echo "Uso: $0 [opções]" |
$1 … $9 | Parâmetros posicionais | arquivo=$1 |
$# | Número de argumentos | [ $# -lt 1 ] && exit 1 |
$@ | Todos os argumentos (lista) | for arg in "$@"; do |
$* | Todos como uma string | Evitar — use $@ |
$? | Exit code do último comando | [ $? -eq 0 ] || exit 1 |
$$ | PID do processo atual | /tmp/lock.$$ |
$! | PID do último background job | wait $! |
$_ | Último argumento do comando anterior | mkdir /tmp/dir && cd $_ |
Processando argumentos
#!/usr/bin/env bash # Exemplo básico de validação de argumentos if [[ $# -lt 2 ]]; then echo "Uso: $0 <origem> <destino>" >&2 exit 1 fi origem="$1" destino="$2" # Iterar sobre todos os argumentos for arg in "$@"; do echo "Argumento: $arg" done
$ ./copia.sh origem.txt "pasta destino" extra Argumento: origem.txt Argumento: pasta destino Argumento: extra # Note: "pasta destino" chegou como UM argumento graças às aspas na chamada # $0=./copia.sh $1=origem.txt $2="pasta destino" $3=extra $#=3
getopts — opções no estilo Unix
#!/usr/bin/env bash verbose=0 output="resultado.txt" while getopts ":vo:" opt; do case $opt in v) verbose=1 ;; o) output="$OPTARG" ;; :) echo "Opção -$OPTARG requer argumento" >&2; exit 1 ;; \?) echo "Opção inválida: -$OPTARG" >&2; exit 1 ;; esac done # Shift para remover as opções processadas shift $(( OPTIND - 1 )) [[ $verbose -eq 1 ]] && echo "Modo verbose ativo. Saída: $output"
getopts: o : inicial em
":vo:" ativa o modo silencioso — em vez de o Bash imprimir sua
própria mensagem de erro, ele coloca o caractere problemático em $OPTARG e
sinaliza opção inválida com ? e argumento faltante com :, deixando
você tratar os dois casos. Cada : depois de uma letra
(como o:) indica que aquela opção exige um argumento.
A variável $OPTIND aponta para o próximo item a processar — por isso o
shift $((OPTIND-1)) ao final deixa apenas os argumentos posicionais.
Testes e comparações
if [[ "$nome" == "root" ]]; then … if [[ "$str" != "" ]]; then … if [[ -z "$var" ]]; then … # vazia if [[ -n "$var" ]]; then … # não vazia if [[ "$a" < "$b" ]]; then … # lexicográfica if [[ "$s" =~ ^[0-9]+$ ]]; then … # regex
-eq igual (equal) -ne diferente (not equal) -lt menor que (less than) -le menor/igual (less or equal) -gt maior que (greater than) -ge maior/igual (greater or equal) if (( a > b )); then … # forma aritmética
-e FILE existe (qualquer tipo) -f FILE existe e é arquivo regular -d FILE existe e é diretório -r FILE existe e é legível -w FILE existe e é gravável -x FILE existe e é executável -s FILE existe e não está vazio -L FILE é um symlink FILE1 -nt FILE2 FILE1 é mais novo FILE1 -ot FILE2 FILE1 é mais antigo
Como o Bash avalia um teste [[ ]]
[[ ]] não é sintaxe mágica: é uma palavra-chave do Bash (ao
contrário de [, que é um comando — na verdade um sinônimo de
test). Entender o fluxo evita a maioria das pegadinhas de quoting:
[[ como palavra-chave e entra em um contexto especial, onde o word splitting e o globbing não ocorrem nos operandos$var vazio continua sendo um operando — por isso [[ -z $var ]] funciona mesmo sem aspas (dentro de [[ ]])== faz casamento de padrão ([[ $f == *.txt ]]), e =~ aplica regex (com grupos em ${BASH_REMATCH[@]})0 se verdadeiro, 1 se falso — que o if então lê[ ] × [[ ]] — quando cada um: use
[[ ]] em scripts Bash (mais seguro: não quebra com variáveis vazias, suporta
&&, ||, == com padrões e =~). Use
[ ] apenas quando precisar de portabilidade POSIX (scripts
#!/bin/sh). Nunca use </> para comparar
números em [[ ]] — ali eles são lexicográficos; para números use
-lt/-gt ou a forma aritmética (( a > b )).
if / elif / else
#!/usr/bin/env bash arquivo="$1" if [[ ! -e "$arquivo" ]]; then echo "ERRO: '$arquivo' não existe." >&2 exit 1 elif [[ -d "$arquivo" ]]; then echo "'$arquivo' é um diretório." elif [[ -f "$arquivo" && -r "$arquivo" ]]; then echo "'$arquivo' é um arquivo legível." else echo "'$arquivo' existe mas não é legível." fi
case / esac
#!/usr/bin/env bash acao="$1" case "$acao" in start|inicio) echo "Iniciando serviço..." ;; stop|parar) echo "Parando serviço..." ;; restart|reiniciar) echo "Reiniciando serviço..." ;; status) echo "Verificando status..." ;; *) echo "Uso: $0 {start|stop|restart|status}" >&2 exit 1 ;; esac
Exit codes: a moeda de troca do shell
Todo comando devolve um código de saída (0–255): 0 significa
sucesso e qualquer valor diferente indica falha. É esse número — guardado em
$? — que if, &&, || e
while leem para decidir o fluxo. Um script deve escolher seu próprio
código com exit N para comunicar o resultado a quem o chamou.
$ grep -q root /etc/passwd; echo "saída: $?" saída: 0 # encontrou → sucesso $ grep -q inexistente /etc/passwd; echo "saída: $?" saída: 1 # não encontrou → falha "esperada" $ ls /pasta/inexistente; echo "saída: $?" ls: cannot access '/pasta/inexistente': No such file or directory saída: 2 # erro real # Encadeamento condicional baseado no exit code $ mkdir -p /tmp/proj && echo "criado" || echo "falhou" criado # Convenções: 0=ok · 1=erro genérico · 2=uso incorreto · 126=sem permissão # 127=comando não encontrado · 130=interrompido (Ctrl+C)
set -e: com set -e ativo, o script aborta
no primeiro comando que retornar código diferente de zero. Mas há exceções:
comandos em if, à esquerda de &&/||, ou negados
com ! não disparam a saída. É por isso que
grep -q algo arquivo || true é o idioma para "tudo bem se não achar".
help test no bash para ver todos os operadores de teste.
for — iteração sobre listas
# Lista literal for cor in vermelho verde azul; do echo "Cor: $cor" done # Glob — iterar arquivos for f in /var/log/*.log; do echo "Log: $f ($(wc -l < "$f") linhas)" done # Sequência numérica for i in $(seq 1 5); do echo "Item $i" done # Estilo C (Bash) for (( i=0; i<5; i++ )); do echo "i=$i" done # Iterar array servidores=("web01" "web02" "db01") for srv in "${servidores[@]}"; do echo "Verificando: $srv" ping -c1 -W1 "$srv" >/dev/null 2>&1 && echo " OK" || echo " FALHOU" done
while — enquanto a condição for verdadeira
# Contador simples contador=1 while [[ $contador -le 5 ]]; do echo "Iteração: $contador" (( contador++ )) done # Leitura de arquivo linha a linha — padrão idiomático while IFS= read -r linha; do echo "Processando: $linha" done < "/etc/passwd" # Leitura da saída de um comando while IFS= read -r usuario; do id "$usuario" 2>/dev/null && echo " Existe: $usuario" done < <(cut -d: -f1 /etc/passwd) # Loop infinito com break while true; do read -rp "Digite 'sair' para terminar: " entrada [[ "$entrada" == "sair" ]] && break echo "Você digitou: $entrada" done
Anatomia do idioma while IFS= read -r linha
A forma canônica de ler um arquivo linha a linha parece críptica, mas cada pedaço tem um motivo. Veja o que cada parte faz — e por que omitir qualquer uma introduz bugs sutis:
IFS= (vazio, só para este comando) desliga o corte de espaços/tabs no início e fim da linha — sem isso, a indentação seria perdidaread -r lê uma linha da entrada; o -r impede que a barra invertida \ seja interpretada como escape (nomes com \ chegam intactos)read retorna sucesso (0) enquanto houver linha e falha (1) ao chegar no EOF — é esse exit code que o while usa para parar< arquivo (ou < <(comando)) alimenta o loop sem criar subshell, então variáveis alteradas dentro dele sobrevivemcomando | while read x; do total=$((total+1)); done
parece contar linhas, mas total volta a zero ao fim do loop! O pipe roda o
while em um subshell, e suas variáveis não escapam. Prefira
while read x; do ...; done < <(comando) (process substitution) — o loop roda
no shell atual e a variável persiste.
until — executa até a condição ser verdadeira
# Aguarda o serviço ficar disponível until curl -sf http://localhost:8080/health >/dev/null 2>&1; do echo "Serviço ainda não disponível, aguardando..." sleep 5 done echo "Serviço UP!"
break e continue
for i in $(seq 1 10); do [[ $i -eq 5 ]] && continue # pula o 5 [[ $i -eq 8 ]] && break # para no 8 echo $i done 1 2 3 4 6 7
Loops que resolvem problemas reais
$ total=0 $ for f in /var/log/*.log; do > n=$(wc -l < "$f") > printf "%-24s %6d linhas\n" "$(basename "$f")" "$n" > total=$(( total + n )) > done alternatives.log 42 linhas dpkg.log 1893 linhas kern.log 671 linhas $ echo "Total: $total linhas" Total: 2606 linhas # Retentativa com backoff — padrão de resiliência $ tentativa=1 $ until ping -c1 -W1 8.8.8.8 >/dev/null 2>&1 || [[ $tentativa -gt 3 ]]; do > echo "Sem rede (tentativa $tentativa), aguardando..." > sleep $(( tentativa * 2 )); (( tentativa++ )) > done Sem rede (tentativa 1), aguardando... Sem rede (tentativa 2), aguardando...
for que chama um comando externo
(como wc) a cada iteração cria um processo novo toda vez — barato para
dezenas de itens, caro para milhares. Quando o volume cresce, prefira uma ferramenta única
que processa tudo de uma vez (awk, wc -l *.log) em vez de um loop
no shell. Loop de shell é cola, não motor de processamento em massa.
for f in $(ls). Se houver nomes com
espaços, o loop se quebra. Use glob diretamente: for f in /caminho/*.
Funções
# Duas sintaxes equivalentes (prefira a primeira) minha_funcao() { echo "Olá da função" } function outra_funcao { echo "Outra forma" } # Funções com argumentos e retorno somar() { local a="$1" # local: escopo restrito à função local b="$2" echo $(( a + b )) # "retorno" via stdout } resultado=$(somar 10 5) echo "10 + 5 = $resultado" 10 + 5 = 15 # Exit code como sinal de sucesso/falha arquivo_existe() { [[ -f "$1" ]] # retorna 0 (true) se existir, 1 se não } if arquivo_existe /etc/passwd; then echo "/etc/passwd existe" fi
return não devolve dados — devolve status: ao contrário de
outras linguagens, return N de uma função Bash define apenas o
exit code (0–255), não um "valor". Para "retornar" texto ou números,
escreva no stdout com echo/printf e capture com
resultado=$(minha_funcao). Use return/exit code para dizer
deu certo? e o stdout para dizer qual é o dado. Misturar os dois — imprimir
logs no stdout de uma função cujo valor você captura — é fonte clássica de bugs; mande logs
para o stderr (>&2).
Padrão de biblioteca de funções
#!/usr/bin/env bash # Funções utilitárias reutilizáveis log() { local level="$1"; shift echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" } log_info() { log "INFO " "$@"; } log_warn() { log "WARN " "$@" >&2; } log_error() { log "ERROR" "$@" >&2; } die() { log_error "$*" exit 1 } # Uso log_info "Script iniciado" [[ -d "$1" ]] || die "Diretório '$1' não encontrado"
Redirecionamento de I/O
# Streams padrão # stdin = fd 0 (entrada) # stdout = fd 1 (saída normal) # stderr = fd 2 (erros) # Redirecionar stdout para arquivo (sobrescrever) ls /etc > lista.txt # Append (acrescentar) ls /var/log >> lista.txt # Redirecionar stderr find / -name "*.conf" 2> erros.txt # Redirecionar stdout e stderr juntos ./script.sh >& tudo.log ./script.sh > saida.log 2>&1 # POSIX — mais portável # Descartar saída comando_silencioso >/dev/null 2>&1 # Entrada de arquivo while IFS= read -r linha; do …; done < arquivo.txt # Here-document cat <<'EOF' Este texto tem $variaveis literais EOF # Here-string read -r nome <<< "Alice"
A ordem importa: >out 2>&1 × 2>&1 >out
O erro de redirecionamento mais comum é inverter a ordem. O shell processa os
redirecionamentos da esquerda para a direita, e 2>&1
significa literalmente "faça o fd 2 apontar para onde o fd 1 aponta agora" — não
"junte os dois para sempre". Acompanhe:
fd 1 → terminal, fd 2 → terminal>out 2>&1: primeiro >out aponta fd 1 → arquivo out; depois 2>&1 aponta fd 2 → onde o fd 1 está agora = out. Resultado: ambos no arquivo. ✓2>&1 >out: primeiro 2>&1 aponta fd 2 → onde o fd 1 está agora = terminal; depois >out muda só o fd 1 → arquivo. Resultado: stdout no arquivo, stderr ainda no terminal. ✗2>&1
vem por último: comando > saida.log 2>&1. A forma
&> saida.log é o atalho do Bash para o mesmo efeito (mas
> ... 2>&1 é o que funciona também em /bin/sh POSIX).
Subshell ( ) × agrupamento { }
Ambos agrupam comandos, mas os parênteses criam um subshell — um processo filho com cópia do ambiente; mudanças (diretório, variáveis) somem ao sair. As chaves agrupam no shell atual, sem processo novo — mais rápido, e o único jeito de um bloco alterar variáveis do script.
$ x=1 $ ( x=99; cd /tmp; echo "dentro: x=$x pwd=$(pwd)" ) dentro: x=99 pwd=/tmp $ echo "fora: x=$x pwd=$(pwd)" # subshell não afetou nada fora: x=1 pwd=/home/user $ { x=99; echo "dentro: x=$x"; } # note o ; antes do } e os espaços dentro: x=99 $ echo "fora: x=$x" # { } alterou a variável do shell fora: x=99
{ } exige espaços internos
e um ; (ou nova linha) antes do } —
{ cmd; }, nunca {cmd}. Já ( ) não precisa de nada
disso. Use ( ) quando quiser isolar efeitos colaterais (ex.:
(cd dir && build) sem mudar o diretório do script) e { } para
apenas redirecionar ou agrupar sem pagar o custo de um processo.
Pipes e process substitution
# Encadeamento de comandos ps aux | grep nginx | awk '{print $2}' | xargs kill -HUP # tee — bifurca: envia para arquivo E stdout ./deploy.sh 2>&1 | tee /var/log/deploy.log # xargs — converte stdin em argumentos find /tmp -name "*.tmp" -mtime +7 | xargs rm -f # Process substitution — trata saída de comando como arquivo diff <(sort arquivo_a.txt) <(sort arquivo_b.txt) # Combinação com operadores lógicos mkdir -p /backup/2026 && cd /backup/2026 || { echo "Falhou"; exit 1; } # Subshell — não afeta o ambiente pai (cd /tmp && ls) # volta ao diretório original após o parêntese
$ diff <(rpm -qa 2>/dev/null | sort) <(cat pacotes_ontem.txt) < nginx-1.24.0 > nginx-1.22.1 # < = só na esquerda; > = só na direita # tee para ver E salvar ao mesmo tempo $ df -h | tee /tmp/disco.txt | grep -E '9[0-9]%|100%' /dev/sda1 20G 19G 1.0G 95% / # só as partições cheias na tela $ wc -l < /tmp/disco.txt # o arquivo guardou a saída completa 6
trap — limpeza ao sair
#!/usr/bin/env bash TMPDIR="$(mktemp -d)" # Garante limpeza em qualquer saída (exit, Ctrl+C, erro) cleanup() { rm -rf "$TMPDIR" echo "Temporários removidos." } trap cleanup EXIT # Sinal Ctrl+C trap 'echo "Interrompido pelo usuário"; exit 130' INT # Uso de $TMPDIR...
trap cleanup EXIT é ouro: o pseudo-sinal
EXIT dispara em qualquer forma de término — fim normal,
exit, erro sob set -e ou Ctrl+C (se você também tratar
INT). Registrar a limpeza logo após criar o recurso (o mktemp -d)
garante que nenhum arquivo temporário fique órfão, não importa por onde o script saia.
É o equivalente shell de um bloco finally.
Here-documents × here-strings
Ambos alimentam o stdin de um comando com texto inline. O
here-document (<<) serve para blocos de várias linhas; o
here-string (<<<) para uma única string curta. O detalhe
decisivo é o quoting do delimitador:
$ nome=Alice # Delimitador SEM aspas → expande variáveis e $(...) $ cat <<EOF > Olá, $nome — hoje é $(date +%A) > EOF Olá, Alice — hoje é Monday # Delimitador com aspas 'EOF' → tudo literal (útil p/ gerar scripts/config) $ cat <<'EOF' > Isto mostra $nome literalmente > EOF Isto mostra $nome literalmente # <<- remove TABs iniciais, permitindo indentar o here-doc no script # Here-string: uma linha direta no stdin $ read -r a b c <<< "um dois três" $ echo "$b" dois $ grep -c root <<< "$(cat /etc/passwd)" 2
export, here-documents); 105.2 — funções, subshells,
substituição de processo e trap.
Laboratórios Guiados
Script de Backup Automatizado com Rotação
Backup incremental com tar, argumentos, validação, log e rotação de cópias antigas
Objetivos do Lab 1
- Processar argumentos com getopts
- Validar existência de diretórios antes de operar
- Gerar nomes de arquivo com timestamp
- Registrar operações em log com timestamps
- Implementar rotação (manter apenas N backups)
#!/usr/bin/env bash # # backup.sh — Backup automatizado com rotação # Uso: ./backup.sh -s <origem> -d <destino> [-n <max_backups>] [-v] # set -euo pipefail # ── Configurações padrão max_backups=5 verbose=0 origem="" destino="" log_file="" # ── Funções utilitárias log() { local msg msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*" echo "$msg" [[ -n "$log_file" ]] && echo "$msg" >> "$log_file" } die() { log "ERRO: $*"; exit 1; } uso() { echo "Uso: $0 -s <origem> -d <destino> [-n <max>] [-v]" echo " -s Diretório de origem" echo " -d Diretório de destino dos backups" echo " -n Máximo de backups a manter (padrão: 5)" echo " -v Modo verboso" exit 1 } # ── Parse de argumentos while getopts ":s:d:n:v" opt; do case $opt in s) origem="$OPTARG" ;; d) destino="$OPTARG" ;; n) max_backups="$OPTARG" ;; v) verbose=1 ;; :) die "Opção -$OPTARG requer argumento" ;; \?) die "Opção inválida: -$OPTARG" ;; esac done # ── Validação [[ -z "$origem" ]] && uso [[ -z "$destino" ]] && uso [[ -d "$origem" ]] || die "Origem não existe: $origem" [[ "$max_backups" =~ ^[0-9]+$ ]] || die "max_backups deve ser inteiro" # ── Preparar destino mkdir -p "$destino" log_file="$destino/backup.log" # ── Gerar nome do arquivo com timestamp timestamp="$(date '+%Y%m%d_%H%M%S')" nome_backup="backup_${timestamp}.tar.gz" caminho_backup="$destino/$nome_backup" log "Iniciando backup: $origem → $caminho_backup" # ── Criar backup tar_opts="czf" [[ $verbose -eq 1 ]] && tar_opts="czvf" if tar "$tar_opts" "$caminho_backup" -C "$(dirname "$origem")" "$(basename "$origem")"; then tamanho="$(du -sh "$caminho_backup" | cut -f1)" log "Backup criado com sucesso: $nome_backup ($tamanho)" else die "Falha ao criar backup" fi # ── Rotação: remover backups antigos log "Verificando rotação (max: $max_backups)" backups_existentes=() while IFS= read -r -d '' b; do backups_existentes+=("$b") done < <(find "$destino" -maxdepth 1 -name "backup_*.tar.gz" -print0 | sort -z) while [[ ${#backups_existentes[@]} -gt $max_backups ]]; do mais_antigo="${backups_existentes[0]}" log "Removendo backup antigo: $(basename "$mais_antigo")" rm -f "$mais_antigo" backups_existentes=("${backups_existentes[@]:1}") done log "Backup concluído. Total de backups: ${#backups_existentes[@]}"
Testando o script
# 1. Criar dados de teste $ mkdir -p ~/dados-teste/{docs,config} $ echo "Arquivo importante" > ~/dados-teste/docs/relatorio.txt $ echo "HOST=localhost" > ~/dados-teste/config/app.conf # 2. Tornar executável $ chmod +x ~/scripts-aula2/backup.sh # 3. Executar o backup $ ~/scripts-aula2/backup.sh -s ~/dados-teste -d /tmp/backups -n 3 -v [2026-06-07 10:30:00] Iniciando backup: /home/user/dados-teste → /tmp/backups/backup_20260607_103000.tar.gz dados-teste/ dados-teste/docs/ dados-teste/docs/relatorio.txt dados-teste/config/ dados-teste/config/app.conf [2026-06-07 10:30:01] Backup criado com sucesso: backup_20260607_103000.tar.gz (1.2K) [2026-06-07 10:30:01] Verificando rotação (max: 3) [2026-06-07 10:30:01] Backup concluído. Total de backups: 1 # 4. Ver log gerado $ cat /tmp/backups/backup.log # 5. Verificar conteúdo do backup $ tar tzf /tmp/backups/backup_*.tar.gz
Limpeza de Logs e Relatório de Uso do Sistema
Dois scripts complementares: limpeza segura de logs antigos e relatório de recursos
Parte A — Limpeza de logs: limpa-logs.sh
#!/usr/bin/env bash # # limpa-logs.sh — Remove logs com mais de N dias # Uso: ./limpa-logs.sh [-d <diretório>] [-a <dias>] [--dry-run] # set -euo pipefail dir_logs="/var/log" max_dias=30 dry_run=0 total_arquivos=0 log() { echo "[$(date '+%H:%M:%S')] $*"; } while [[ $# -gt 0 ]]; do case "$1" in -d) dir_logs="$2"; shift 2 ;; -a) max_dias="$2"; shift 2 ;; --dry-run) dry_run=1; shift ;; *) echo "Opção desconhecida: $1" >&2; exit 1 ;; esac done [[ -d "$dir_logs" ]] || { echo "Diretório não existe: $dir_logs" >&2; exit 1; } [[ $dry_run -eq 1 ]] && log "*** MODO DRY-RUN — nenhum arquivo será removido ***" log "Varrendo '$dir_logs' por logs com mais de $max_dias dias..." while IFS= read -r arquivo; do tamanho="$(du -sh "$arquivo" 2>/dev/null | cut -f1)" total_arquivos=$(( total_arquivos + 1 )) if [[ $dry_run -eq 1 ]]; then log "[DRY-RUN] Removeria: $arquivo ($tamanho)" else log "Removendo: $arquivo ($tamanho)" rm -f "$arquivo" fi done < <(find "$dir_logs" -maxdepth 2 -name "*.log" -mtime +"${max_dias}" -type f) log "Concluído. Arquivos processados: $total_arquivos"
Parte B — Relatório de uso: relatorio-uso.sh
#!/usr/bin/env bash # # relatorio-uso.sh — Relatório completo de uso do sistema # Uso: ./relatorio-uso.sh [-o <arquivo-saida>] # set -euo pipefail output="" [[ "${1:-}" == "-o" ]] && output="$2" [[ -n "$output" ]] && exec > >(tee "$output") 2>&1 separator() { printf '%s\n' "$(printf '─%.0s' {1..60})"; } header() { separator echo " $1" separator } echo "╔══════════════════════════════════════════════════════════╗" echo "║ RELATÓRIO DE USO DO SISTEMA ║" echo "╚══════════════════════════════════════════════════════════╝" echo "Gerado em : $(date '+%Y-%m-%d %H:%M:%S')" echo "Hostname : $(hostname -f 2>/dev/null || hostname)" echo "Usuário : $(whoami)" echo header "SISTEMA" source /etc/os-release 2>/dev/null || true echo "Distro : ${PRETTY_NAME:-desconhecida}" echo "Kernel : $(uname -r)" echo "Arch : $(uname -m)" echo "Uptime : $(uptime -p 2>/dev/null || uptime)" echo header "CPU" echo "Modelo : $(grep -m1 'model name' /proc/cpuinfo | cut -d: -f2 | xargs)" echo "Núcleos : $(nproc)" echo "Carga : $(cat /proc/loadavg)" echo header "MEMÓRIA" free -h echo header "DISCO — Partições montadas" df -hT --type=ext4 --type=xfs --type=btrfs 2>/dev/null || df -hT echo header "TOP 5 DIRETÓRIOS EM /var" du -sh /var/* 2>/dev/null | sort -rh | head -5 echo header "TOP 5 PROCESSOS (CPU)" ps -eo pid,user,%cpu,%mem,comm --sort=-%cpu | head -6 echo if command -v systemctl >/dev/null 2>&1; then header "SERVIÇOS ATIVOS (systemd)" systemctl list-units --type=service --state=running --no-pager --no-legend \ | awk '{print $1, $4}' | head -10 echo fi header "RESUMO DE LOGS (/var/log)" for log_file in /var/log/syslog /var/log/auth.log /var/log/kern.log; do [[ -f "$log_file" ]] || continue linhas="$(wc -l < "$log_file")" tamanho="$(du -sh "$log_file" | cut -f1)" printf "%-30s %6s linhas %s\n" "$(basename "$log_file")" "$linhas" "$tamanho" done echo separator echo "Relatório concluído." [[ -n "$output" ]] && echo "Salvo em: $output"
Executando o Lab 2
# Criar os scripts $ cd ~/scripts-aula2 $ chmod +x limpa-logs.sh relatorio-uso.sh # Testar limpeza em modo dry-run (seguro) $ ./limpa-logs.sh -d /tmp -a 1 --dry-run # Gerar relatório na tela $ ./relatorio-uso.sh # Gerar relatório e salvar em arquivo $ ./relatorio-uso.sh -o /tmp/relatorio-$(date +%Y%m%d).txt $ cat /tmp/relatorio-*.txt
Erros Comuns e Como Depurar
set -x — modo rastreamento
set -x # ativa + echo "teste" teste # Ativar só para um bloco { set -x comando_suspeito set +x; } 2>/tmp/trace.log # Na invocação $ bash -x ./script.sh
set -euo pipefail
set -e # exit em qualquer erro set -u # erro em variável não definida set -o pipefail # pipe propaga erros # Verificar sintaxe sem executar $ bash -n script.sh script.sh: line 15: syntax error
shellcheck
$ shellcheck script.sh In script.sh line 5: for f in $(ls /tmp); do ^-- SC2045: Iterating over ls output # Instalar $ sudo apt install shellcheck # Em CI: $ shellcheck -S error *.sh
Tabela de erros comuns
| Erro / Sintoma | Causa | Correção |
|---|---|---|
syntax error near unexpected token `fi' |
Falta de then ou ponto-e-vírgula no if |
if [[ ... ]]; then — note o ponto-e-vírgula |
command not found em script com chmod +x |
Shebang faltando ou errado (#!/bin/bash com CRLF) |
Adicionar #!/usr/bin/env bash; converter com dos2unix |
| Variável vazia inesperada | Espaço ao redor do =: var = "valor" |
Sem espaços: var="valor" |
| Loop não processa nomes com espaços | for f in $(ls) expande incorretamente |
Usar glob: for f in /dir/* com IFS correto |
[: too many arguments |
Variável não citada em [ $var = "x" ] |
Sempre citar: [[ "$var" = "x" ]] |
| Pipe falha mas o script continua | Bash ignora erros de processos intermediários no pipe | Adicionar set -o pipefail no cabeçalho |
| Script não termina com Ctrl+C | Subprocesso captura o sinal | Usar trap 'kill 0' INT para propagar ao grupo |
| Arquivo temporário deixado para trás em erro | Sem cleanup em saída de erro | Usar trap cleanup EXIT |
Desafio Final
Duração: 20 minutos |
Formato: Individual |
Entregável: ~/scripts-aula2/monitor.sh — funcional e sem erros no shellcheck
Missão
Escreva um script monitor.sh que execute as verificações abaixo e emita alertas quando os limites forem ultrapassados. O script deve aceitar os limites como argumentos opcionais e gravar um log com timestamp.
Requisitos obrigatórios
- Aceitar flags:
-c <%cpu>-m <%mem>-d <%disco>com valores padrão razoáveis - Verificar uso de CPU (via
/proc/loadavgoutop -bn1) e alertar se > limite - Verificar uso de memória (via
free) e alertar se > limite - Verificar todas as partições montadas e alertar se uso > limite
- Usar pelo menos uma função, um loop e um condicional
- Gravar log em
/tmp/monitor.logcom timestamp em cada entrada - Sair com código
0se tudo OK,1se houver algum alerta
Esqueleto inicial
#!/usr/bin/env bash set -euo pipefail # Limites padrão (em %) lim_cpu=80 lim_mem=85 lim_disco=90 log_file="/tmp/monitor.log" alertas=0 log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$log_file"; } alerta() { log "[ALERTA] $*"; alertas=$(( alertas + 1 )); } # Parse de argumentos (complete com getopts) # ... verificar_cpu() { # Dica: use 'top -bn1' e awk para extrair o % de uso # ... } verificar_memoria() { # Dica: use 'free' para calcular % usado = used / total * 100 # ... } verificar_disco() { # Dica: use 'df -h' e awk para checar cada partição # ... } # Execução log "Monitor iniciado (cpu:${lim_cpu}% mem:${lim_mem}% disco:${lim_disco}%)" verificar_cpu verificar_memoria verificar_disco if [[ $alertas -eq 0 ]]; then log "Tudo OK. Nenhum alerta gerado." exit 0 else log "$alertas alerta(s) gerado(s)." exit 1 fi
Bônus — para quem terminar antes
# 1. Adicionar flag --loop <segundos> para rodar em laço contínuo # 2. Colorir alertas no terminal com códigos ANSI # 3. Adicionar verificação de porta aberta: nc -z host porta # 4. Enviar e-mail de alerta se postfix estiver configurado # Após concluir, validar com shellcheck: $ shellcheck ~/scripts-aula2/monitor.sh
Rubrica de Correção
Pontuação Total
Executa scripts de exemplo; adapta com orientação; entende variáveis e condicionais mas ainda tem dificuldade com loops e redirecionamento.
Escreve scripts funcionais com funções e tratamento básico de erro; usa shellcheck; os labs 1 e 2 estão completos e funcionais.
Scripts robustos com tratamento completo de erros, trap, getopts e zero avisos no shellcheck; desafio com bônus concluído; capaz de explicar cada linha.
Referência Rápida de Shell Scripting
Cabeçalho padrão
#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t'
Expansões úteis
${var:-padrao} # padrão
${var:?msg} # obrigatória
${#var} # comprimento
${var%sufixo} # remove sufixo
${var#prefixo} # remove prefixo
Redirecionamento
> out.txt # stdout → arquivo >> out.txt # append 2> err.txt # stderr → arquivo &> all.txt # ambos &>/dev/null # silenciar tudo tee log.txt # bifurca stdout
Depuração
bash -n script # checa sintaxe
bash -x script # rastreia
set -x / set +x # bloco
shellcheck *.sh # análise estática
echo "${LINENO}" # número da linha
Padrões comuns
cmd || die "msg" # falha=fatal
cmd && seguir # só se OK
${1:?'arg obrigat.'} # validar arg
mktemp # arquivo temp
trap cleanup EXIT # garantir cleanup
Filtros essenciais
awk '{print $2}' # coluna 2
cut -d: -f1 # campo 1 (delim :)
grep -E 'padrão' # regex estendida
sed 's/de/para/g' # substituição
sort -rh # sort humano reverso
Checklist de Avaliação Prática
Tarefa de Fixação (para casa)
Entrega: próxima aula
Entregue um arquivo tarefa02.sh — script executável, sem erros de sintaxe
e com zero avisos do shellcheck.
Requisitos
- Cabeçalho com
#!/usr/bin/env basheset -euo pipefail - Aceitar
-h(help),-v(verbose) e-o arquivo(saída) via getopts - Função
log()que registra timestamp + mensagem em stdout e em arquivo - Loop que itera sobre os 5 maiores processos e lista
PID:nome:cpu% - Verificar com
ifse cada partição está acima de 80% e emitir aviso trap cleanup EXITremovendo arquivos temporários- Sair com código
0se sem alertas,2se houver alertas - Bônus: Aceitar lista de hosts como argumentos e verificar se estão online com
ping -c1
Recursos e Referências
LPI (referência principal)
- 📄 LPIC-1 Objective 105.2 — Customize or write simple scripts
- 📄 LPIC-1 Objective 103.4 — Use streams, pipes and redirects
- 📄 Linux Essentials Topic 3.3 — Turning Commands into Scripts
Documentação e ferramentas
- 📖
man bash— seções VARIABLES, EXPANSION, FUNCTIONS - 🔧 shellcheck.net — análise online
- 📖
help getopts— builtin do bash
Leitura complementar
- 📚 "The Linux Command Line" — W. Shotts, cap. 24–36
- 📚 Google Shell Style Guide (google.github.io/styleguide/shellguide.html)
- 📚 Bash Pitfalls — mywiki.wooledge.org/BashPitfalls
Notas do Instrutor
Dicas para condução da aula
- Execute o shellcheck ao vivo enquanto escreve — torna a ferramenta natural e não punitiva
- No Bloco 2, provoque o erro de variável sem aspas (
[ $var = x ]) com um nome com espaço — o erro visual convence melhor que qualquer explicação - No Lab 1, peça aos alunos que rodem o backup 6 vezes e observem a rotação acontecendo — visualizar o log torna o conceito concreto
- Mostre
bash -xem um script com bug proposital — o rastreamento deve revelar o erro sem o aluno precisar adivinhar - No desafio, deixe os alunos tentarem por 10 minutos antes de dar qualquer dica — errar e debugar é a melhor parte do aprendizado
- Relacione cada estrutura ao LPIC-1 Objective 105.2 — cria consciência do caminho de certificação
Erros comuns a antecipar
- Espaços ao redor do
=na atribuição:var = "valor"— mostrar a mensagem de erro e a correção - Usar
[ ]em vez de[[ ]]com strings que podem ser vazias - Esquecer de citar
"$@"nos loops — demonstrar com nome de arquivo com espaço - Confundir exit code
0=sucessocom o comportamento de linguagens de programação - Colocar a lógica de redirecionamento após o comando (
echo foo 2>&1 >outvs>out 2>&1)
Pontuação Sugerida
Escreve scripts lineares com echo e variáveis; usa condicionais simples
com orientação; ainda confunde aspas, expansão e exit codes.
Usa condicionais, loops, funções e argumentos posicionais sem ajuda; trata exit codes;
depura com bash -x e resolve o desafio consultando a documentação.
Domina expansão de parâmetros, arrays, trap, here-docs e process
substitution; escreve scripts robustos com set -euo pipefail e cleanup.
Orientações Gerais
Como aproveitar melhor este módulo
- Rode cada script com
bash -x script.shpara ver a expansão acontecer linha a linha — é o que torna as etapas do Bash concretas - Sempre cite variáveis (
"$var","$@"): a maioria dos bugs de shell nasce de word splitting e globbing inesperados - Comece todo script sério com
set -euo pipefaile umtrap cleanup EXIT— falhe cedo e limpe sempre - Transforme um comando manual que você repete em um script: fixar o aprendizado exige escrever, não só ler
- Use
help nomepara builtins eman bash(seção Parameter Expansion) como referência ao lado
Erros comuns a evitar
- Colocar espaços ao redor do
=na atribuição (x = 1é interpretado como comando) - Usar
[ ]com uma variável possivelmente vazia sem aspas — prefira[[ ]] - Esquecer o
chmod +xou o shebang#!/usr/bin/env bash - Confundir
>(sobrescreve) com>>(anexa) e trocar a ordem de2>&1