Shell Scripting e Automação

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/else e case com testes robustos
  • Implementar loops for, while e until
  • 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 -n e shellcheck

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, vim ou cat)
  • Entender pipes (|) e redirecionamento básico (>, >>)

Ambiente necessário

  • VM com Ubuntu 22.04 LTS ou Debian 12 com Bash 5+
  • shellcheck instalado: sudo apt install shellcheck
  • Diretório de trabalho criado: mkdir -p ~/scripts-aula2
  • Editor preferido disponível no PATH
Compatibilidade com o laboratório (container LXD não privilegiado)
  • Este módulo roda 100% dentro do container — shell scripting não depende de nenhuma capacidade privilegiada.
00:10 – 00:35 Bloco 1 — Variáveis, Expansões e Variáveis Especiais 25 min

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.

Declaração, atribuição e leitura
# 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
Pegadinha dos dois-pontos: ${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:

Expansão de chaves{a,b,c} e {1..5} geram as sequências (é puramente textual, ocorre primeiro)
Expansão de til~ e ~usuario viram caminhos de home
Expansão de parâmetros/variáveis$var, ${var:-x}, ${#var}
Substituição de comando$(...) é executado e substituído pela saída
Expansão aritmética$(( ... )) é calculado
Word splitting — o resultado é quebrado em palavras segundo o IFS (só fora de aspas!)
Expansão de nome de arquivo (globbing)*, ?, [...] viram nomes reais que existem no disco
Remoção de aspas — as aspas restantes são retiradas antes de o comando finalmente rodar
Por que aspas importam: word splitting e globbing acontecem depois da expansão de variáveis. Por isso rm $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.

Manipulando arrays na prática
$ 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
Insight: a diferença entre "${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ávelSignificadoExemplo de uso
$0Nome do scriptecho "Uso: $0 [opções]"
$1 … $9Parâmetros posicionaisarquivo=$1
$#Número de argumentos[ $# -lt 1 ] && exit 1
$@Todos os argumentos (lista)for arg in "$@"; do
$*Todos como uma stringEvitar — use $@
$?Exit code do último comando[ $? -eq 0 ] || exit 1
$$PID do processo atual/tmp/lock.$$
$!PID do último background jobwait $!
$_Último argumento do comando anteriormkdir /tmp/dir && cd $_
LPI Reference: LPIC-1 Objective 105.2 — Customize or write simple scripts (variáveis, substituição, quoting). Linux Essentials Topic 3.3 — Turning Commands into Scripts.
00:35 – 01:05 Bloco 2 — Argumentos, Condicionais e Testes 30 min (prático)

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
Como o script enxerga os argumentos ao ser chamado
$ ./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"
Os dois-pontos do 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

Comparações de string
if [[ "$nome" == "root" ]]; thenif [[ "$str" != "" ]]; thenif [[ -z "$var" ]]; then# vazia
if [[ -n "$var" ]]; then# não vazia
if [[ "$a" < "$b" ]]; then# lexicográfica
if [[ "$s" =~ ^[0-9]+$ ]]; then# regex
Comparações numéricas
-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
Testes de arquivo e diretório
-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:

O Bash reconhece [[ como palavra-chave e entra em um contexto especial, onde o word splitting e o globbing não ocorrem nos operandos
As variáveis são expandidas, mas $var vazio continua sendo um operando — por isso [[ -z $var ]] funciona mesmo sem aspas (dentro de [[ ]])
O operador é avaliado: == faz casamento de padrão ([[ $f == *.txt ]]), e =~ aplica regex (com grupos em ${BASH_REMATCH[@]})
O resultado vira um exit code: 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.

Exit codes na prática
$ 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)
Aha do 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".
LPI Reference: LPIC-1 Objective 105.2 — if/then, [ ], [[ ]], case; use o comando help test no bash para ver todos os operadores de teste.
01:05 – 01:30 Bloco 3 — Loops: for, while, until 25 min (prático)

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 perdida
read -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
O redirecionamento < arquivo (ou < <(comando)) alimenta o loop sem criar subshell, então variáveis alteradas dentro dele sobrevivem
Armadilha do subshell: comando | 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

Contar linhas de cada log e somar o total
$ 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...
Insight de desempenho: um 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.
Armadilha: nunca use for f in $(ls). Se houver nomes com espaços, o loop se quebra. Use glob diretamente: for f in /caminho/*.
LPI Reference: LPIC-1 Objective 105.2 — Loops com for, while e until; uso correto de IFS ao ler arquivos.
01:45 – 02:15 Bloco 4 — Funções, Pipes e Redirecionamento 30 min

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:

Início: fd 1 → terminal, fd 2 → terminal
Em >out 2>&1: primeiro >out aponta fd 1 → arquivo out; depois 2>&1 aponta fd 2onde o fd 1 está agora = out. Resultado: ambos no arquivo.
Em 2>&1 >out: primeiro 2>&1 aponta fd 2onde o fd 1 está agora = terminal; depois >out muda só o fd 1 → arquivo. Resultado: stdout no arquivo, stderr ainda no terminal.
Regra prática: para capturar tudo em um arquivo, o 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.

A diferença visível
$ 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
Detalhe de sintaxe: { } 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
Process substitution na prática — comparar duas listas
$ 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...
Por que 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:

Expansão ligada e desligada
$ 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
LPI Reference: LPIC-1 Objective 105.1 — Customize and use the shell environment (variáveis, export, here-documents); 105.2 — funções, subshells, substituição de processo e trap.
LPI Reference: LPIC-1 Objective 103.4 — Use streams, pipes and redirects; 105.2 — Functions, trap and process substitution.

Laboratórios Guiados

01

Script de Backup Automatizado com Rotação

Backup incremental com tar, argumentos, validação, log e rotação de cópias antigas

45 min Individual Entregável: ~/scripts-aula2/backup.sh

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)
Script completo — backup.sh
#!/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
Ponto de verificação: Execute o backup 4 vezes e verifique que apenas 3 backups foram mantidos. Inspecione o log para confirmar as mensagens de rotação.
02

Limpeza de Logs e Relatório de Uso do Sistema

Dois scripts complementares: limpeza segura de logs antigos e relatório de recursos

40 min Individual 2 scripts entregáveis

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
Ponto de verificação: O relatório deve exibir todas as seções sem erros. Execute o limpa-logs em dry-run e interprete a saída antes de usar sem a flag.

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 / SintomaCausaCorreçã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

⚡ Desafio — "Monitor de Saúde do Sistema"

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/loadavg ou top -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.log com timestamp em cada entrada
  • Sair com código 0 se tudo OK, 1 se 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

Dimensão Satisfatório (2 pts) Bom (3 pts) Excelente (5 pts)
Legibilidade Variáveis nomeadas; indentação consistente Funções com nomes descritivos; estrutura clara; shebang correto Cabeçalho com uso; funções coesas; zero comentários redundantes; segue ShellCheck
Robustez Script executa sem errar no caminho feliz set -euo pipefail; valida argumentos e existência de arquivos Trata todos os edge cases; usa trap para cleanup; sai com código correto; logs com timestamp
Segurança básica Não usa eval; não expande variáveis sem aspas em rm/cp/mv Cita todas as variáveis expandidas; usa -- antes de globs em rm Valida entradas externas; usa mktemp para temporários; permissões corretas nos scripts (700)
Correção funcional Produz a saída esperada no caso básico Passa em todos os casos de teste do enunciado Funciona com nomes com espaços; bônus implementado
Estruturas obrigatórias Usa pelo menos variável, loop e condicional Adiciona função e redirecionamento adequado Usa getopts, arrays, trap, process substitution e funções com local

Pontuação Total

0–12 pts — Iniciante

Executa scripts de exemplo; adapta com orientação; entende variáveis e condicionais mas ainda tem dificuldade com loops e redirecionamento.

13–18 pts — Intermediário

Escreve scripts funcionais com funções e tratamento básico de erro; usa shellcheck; os labs 1 e 2 estão completos e funcionais.

19–25 pts — Avançado

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

Critério Iniciante Intermediário Avançado
Variáveis Declara e usa variáveis simples com $var Usa expansões ${var:-padrao} e variáveis especiais $#, $@, $? Usa arrays, expansões de transformação e local em funções
Condicionais Escreve if [[ ]] com comparações de string simples Usa testes de arquivo, comparações numéricas e case Combina condicionais com &&/||, regex com =~ e getopts
Loops Escreve for sobre lista literal Usa while read para processar arquivos; break/continue Process substitution; loops robustos com IFS; iterar arrays
Funções Define e chama funções simples Usa local, captura saída com $() e retorna exit codes Biblioteca de funções reutilizável com log, die e cleanup padronizados
I/O e pipes Usa > e | em comandos simples Redireciona stderr; usa tee; here-documents Process substitution; pipelines complexos; exec para redirecionamento global
Robustez Script funciona no caminho feliz set -euo pipefail; valida argumentos e arquivos trap para cleanup; trata sinais; zero avisos no shellcheck
Scripts reais Adapta scripts fornecidos com pequenas mudanças Escreve backup.sh e relatorio-uso.sh funcionais Monitor completo com getopts, alertas e log; bônus implementado

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 bash e set -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 if se cada partição está acima de 80% e emitir aviso
  • trap cleanup EXIT removendo arquivos temporários
  • Sair com código 0 se sem alertas, 2 se 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 -x em 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=sucesso com o comportamento de linguagens de programação
  • Colocar a lógica de redirecionamento após o comando (echo foo 2>&1 >out vs >out 2>&1)

Pontuação Sugerida

Iniciante — 0 a 5 pts

Escreve scripts lineares com echo e variáveis; usa condicionais simples com orientação; ainda confunde aspas, expansão e exit codes.

Intermediário — 6 a 8 pts

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.

Avançado — 9 a 10 pts

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.sh para 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 pipefail e um trap 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 nome para builtins e man 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 +x ou o shebang #!/usr/bin/env bash
  • Confundir > (sobrescreve) com >> (anexa) e trocar a ordem de 2>&1