A Oficina de Scripts — 43 Passos

Objetivos de Aprendizagem

Este exercício põe a mão em tudo o que o Módulo 02 — Shell Scripting e Automação apresentou: variáveis e expansões, argumentos, condicionais, loops, funções, redirecionamento, pipes, trap e o cabeçalho defensivo set -euo pipefail. A história continua a do Exercício 03: você é o técnico do Laboratório Bit Livre e cansou de digitar as fichas de inventário à mão — agora vai escrever os scripts que fazem isso por você. Ao final, você será capaz de:

  • Escrever um script executável do zero: shebang, chmod +x e execução por caminho
  • Explicar a diferença entre ./script.sh, bash script.sh e source script.sh
  • Usar variáveis com o quoting correto e prever quando o shell divide uma palavra em várias
  • Aplicar expansões ${var:-padrão}, ${#var}, ${var/a/b} e aritmética $(( ))
  • Ler argumentos com $1, $#, "$@", shift e getopts
  • Escrever testes de string, número, arquivo e diretório e ramificar com if e case
  • Percorrer listas, arquivos e contadores com for, while e until
  • Ler um arquivo linha a linha do jeito certo — while IFS= read -r linha
  • Criar funções com local, devolver dados por stdout e status por exit code
  • Redirecionar stdout e stderr na ordem correta e usar tee, pipes e here-documents
  • Garantir limpeza com trap ... EXIT e depurar com set -x e shellcheck
  • Montar, ao final, um script de relatório que usa todos esses recursos juntos

Antes de Começar

Ambiente necessário

  • Acesso à sua VM Ubuntu via SSH ou terminal local
  • Usuário comum — nenhum passo precisa de root
  • Shell Bash: confirme com echo $SHELL e bash --version
  • Um editor de texto: nano serve (aqui ele é permitido)
  • shellcheck no passo 42 — se não tiver, use shellcheck.net
  • Pré-requisitos: Módulo 02 e os Exercícios 0103

Regras do jogo

  • Digite os scripts — não copie e cole. O erro de digitação é parte da aula.
  • Aqui o editor é liberado: script de verdade não nasce de echo
  • Depois de todo chmod +x, rode o script e confira o echo $?
  • Antes do Enter, diga em voz alta qual vai ser o exit code
  • Todo script deste exercício começa com #!/usr/bin/env bash
  • Ctrl+O salva e Ctrl+X sai do nano
Sobre as saídas mostradas: foram conferidas no Bash 5.x sobre Ubuntu 24.04. Datas, PIDs, tamanhos e percentuais serão diferentes na sua máquina — o que precisa bater é a forma da saída, não os números. Onde a saída depende da máquina, o roteiro avisa.
Por que este exercício erra de propósito: nos passos 7, 9, 16, 21 e 40 você vai escrever um comando errado antes de escrever o certo. Não pule essas duplas: a mensagem de erro do Bash é o conteúdo mais denso da disciplina, e reconhecê-la de imediato vale mais do que decorar sintaxe.

Cronograma do Exercício

Horário Parte Passos Assunto em foco Duração
00:00 – 00:18 A — O primeiro script 1 – 6 shebang, chmod +x, PATH, $? 18 min
00:18 – 00:45 B — Variáveis e expansões 7 – 14 Aspas, ${ }, $( ), $(( )), arrays 27 min
00:45 – 01:15 C — Argumentos e decisões 15 – 23 $#, "$@", shift, getopts, if, case 30 min
01:15 – 01:40 D — Repetição 24 – 30 for, while, until, read -r 25 min
01:40 – 02:10 E — Funções e I/O 31 – 38 local, biblioteca de log, redirecionamento, pipes, here-doc 30 min
02:10 – 02:30 F — Robustez e entrega 39 – 43 set -x, set -euo pipefail, trap, shellcheck 20 min

Mapa da Estrutura Final

O Laboratório Bit Livre cresceu: agora são seis máquinas e o inventário passou a viver em um arquivo CSV. Sua missão é construir a oficina de scripts que lê esse arquivo, valida, filtra, conta e gera relatório — sem que ninguém precise abrir uma planilha. Este é o destino final:

/home/aluno/oficina/
├── bin/
│   ├── ola.sh              (passo 2  — o primeiro script)
│   ├── ficha.sh            (passo 17 — gera ficha a partir de argumentos)
│   ├── busca.sh            (passo 23 — busca por setor, com case)
│   ├── lib-log.sh          (passo 34 — biblioteca de funções, sem shebang)
│   └── relatorio.sh        (passo 43 — o script integrador)
├── dados/
│   ├── maquinas.csv        (6 máquinas + cabeçalho)
│   └── setores.txt         (3 linhas)
├── fichas/                 (populado pelos scripts, nunca à mão)
└── logs/
    └── oficina.log         (escrito por lib-log.sh via tee)
Ao final devem existir 5 scripts em bin/ — quatro deles executáveis e lib-log.sh não executável, porque biblioteca se carrega com source, não se executa —, 6 fichas em fichas/ e um logs/oficina.log com pelo menos uma linha por execução.

Roteiro Passo a Passo

00:00 – 00:18 Parte A — O primeiro script (passos 1 a 6) 18 min

Um script é só um arquivo de texto com duas coisas a mais: uma primeira linha dizendo quem deve interpretá-lo e uma permissão dizendo que ele pode ser executado. Esta parte instala esses dois hábitos antes de qualquer sintaxe.

Passo 1 — Montar a oficina (mkdir -p, cd, pwd)

# Comece do home, não importa onde você estivesse
$ cd
$ mkdir -p oficina/bin oficina/dados oficina/fichas oficina/logs
$ cd oficina
$ pwd
/home/aluno/oficina

$ ls -F
bin/  dados/  fichas/  logs/

Passo 2 — O primeiro script e o shebang

$ nano bin/ola.sh
Conteúdo de bin/ola.sh
#!/usr/bin/env bash
# ola.sh — o menor script útil que existe

echo "Oficina Bit Livre — script no ar"
echo "Rodando como: $(whoami)"
echo "Diretório atual: $(pwd)"
A primeira linha é o shebang (#!). Ela não é comentário: o kernel a lê para descobrir qual interpretador chamar. #!/usr/bin/env bash é preferível a #!/bin/bash porque procura o bash no PATH — funciona igual em Linux, macOS e BSD, onde o caminho absoluto muda.

Passo 3 — Tente executar antes de dar permissão (erro proposital)

$ ./bin/ola.sh
bash: ./bin/ola.sh: Permission denied

# O arquivo existe e é legível — o que falta é o bit de execução
$ ls -l bin/ola.sh
-rw-rw-r-- 1 aluno aluno 168 Sep 15 10:02 bin/ola.sh

$ chmod +x bin/ola.sh
$ ls -l bin/ola.sh
-rwxrwxr-x 1 aluno aluno 168 Sep 15 10:02 bin/ola.sh

$ ./bin/ola.sh
Oficina Bit Livre — script no ar
Rodando como: aluno
Diretório atual: /home/aluno/oficina

Compare as duas saídas de ls -l: o que mudou foram os três x. Repare também que pwd imprimiu /home/aluno/oficina, e não /home/aluno/oficina/bino script roda no diretório de quem o chamou, não no diretório onde ele mora. Esquecer disso é a causa número um de "funciona aqui e não funciona no cron".

Passo 4 — Por que o ./? (o PATH)

$ ola.sh
bash: ola.sh: command not found

# O shell só procura executáveis nos diretórios listados no PATH
$ echo "$PATH"
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

# O diretório atual NÃO está lá — por segurança. Daí o ./
$ type -a ./bin/ola.sh
./bin/ola.sh is ./bin/ola.sh

# Adicione a oficina ao PATH só nesta sessão
$ export PATH="$PWD/bin:$PATH"
$ ola.sh
Oficina Bit Livre — script no ar
Rodando como: aluno
Diretório atual: /home/aluno/oficina

$ which ola.sh
/home/aluno/oficina/bin/ola.sh
Esse export vale só nesta janela de terminal. Ao abrir outra, o PATH volta ao normal e o ola.sh some. Tornar permanente é assunto de ~/.bashrc — e nunca coloque . (diretório atual) no PATH: qualquer arquivo chamado ls numa pasta em que você entrasse seria executado no lugar do ls real.

Passo 5 — ./script × bash script × source script

# 1) ./ — precisa de +x e do shebang; roda em um processo NOVO
$ ./bin/ola.sh

# 2) bash arquivo — dispensa +x e ignora o shebang; também é processo novo
$ bash bin/ola.sh

# 3) source (ou .) — roda no shell ATUAL, sem processo filho
$ source bin/ola.sh
$ . bin/ola.sh          # exatamente a mesma coisa
A diferença que importa — prove você mesmo
$ echo 'cd /tmp' > /tmp/vai.sh

$ bash /tmp/vai.sh
$ pwd
/home/aluno/oficina   # o cd aconteceu no processo filho e morreu com ele

$ source /tmp/vai.sh
$ pwd
/tmp                  # o cd aconteceu AQUI

$ cd ~/oficina         # volte para a oficina antes de seguir
Guarde a regra: script que faz um trabalho se executa com ./; arquivo que define variáveis e funções para o shell atual se carrega com source. É por isso que o lib-log.sh do passo 34 não vai receber chmod +x.

Passo 6 — Exit code: o valor de retorno de todo comando ($?)

$ ls /etc >/dev/null
$ echo $?
0                     # 0 = sucesso. SEMPRE.

$ ls /pasta-que-nao-existe 2>/dev/null
$ echo $?
2                     # qualquer coisa != 0 = falha

$ grep -q root /etc/passwd; echo $?
0
$ grep -q pinguim /etc/passwd; echo $?
1
Um script que escolhe o próprio exit code
$ nano /tmp/teste-saida.sh
#!/usr/bin/env bash
echo "Vou sair com 3"
exit 3
$ bash /tmp/teste-saida.sh; echo "codigo=$?"
Vou sair com 3
codigo=3

# O exit code é o que faz && e || funcionarem
$ true  && echo "roda quando o anterior deu certo"
roda quando o anterior deu certo
$ false || echo "roda quando o anterior falhou"
roda quando o anterior falhou
$? guarda o código do último comando executado e é sobrescrito imediatamente. Se você precisar dele duas vezes, salve na hora: rc=$?. Um echo no meio já destruiu o valor que você queria.
00:18 – 00:45 Parte B — Variáveis e expansões (passos 7 a 14) 27 min

Tudo em Bash é texto até que se prove o contrário. Esta parte é sobre uma única pergunta que você vai fazer pelo resto da vida: quando o shell divide uma palavra em várias, e como impedir isso? A resposta cabe em dois caracteres: ".

Passo 7 — Atribuição não tem espaço (erro proposital)

$ nome = "Alice"
bash: nome: command not found

# O shell leu "nome" como um COMANDO e "=" e "Alice" como argumentos
$ nome="Alice"
$ echo "$nome"
Alice

# Sem espaço dos dois lados do = . Nunca.
$ versao="2.1"
$ contador=0
$ echo "versao=$versao contador=$contador"
versao=2.1 contador=0

Passo 8 — Chaves, readonly e unset

$ setor="rede"

# Sem chaves, o shell acha que a variável se chama "setor_lab"
$ echo "$setor_lab"

# Com chaves, fica explícito onde o nome termina
$ echo "${setor}_lab"
rede_lab

# readonly: constante — não pode mais mudar
$ readonly MAX_MAQUINAS=6
$ MAX_MAQUINAS=99
bash: MAX_MAQUINAS: readonly variable

# unset: apaga a variável
$ unset contador
$ echo "[${contador}]"
[]
Use ${ } por padrão. Custa dois caracteres e elimina toda uma classe de bug em que o shell "come" parte do nome da variável porque o caractere seguinte também era válido em um identificador.

Passo 9 — Aspas: a aula mais importante da página (erro proposital)

$ arquivo="relatorio de setembro.txt"
$ touch "$arquivo"
$ ls
bin  dados  fichas  logs  'relatorio de setembro.txt'

# SEM aspas: o shell divide nos espaços e rm recebe TRÊS argumentos
$ rm $arquivo
rm: cannot remove 'relatorio': No such file or directory
rm: cannot remove 'de': No such file or directory
rm: cannot remove 'setembro.txt': No such file or directory

# COM aspas: um argumento só, como você queria
$ rm "$arquivo"
$ ls -F
bin/  dados/  fichas/  logs/
Aspas duplas × aspas simples × sem aspas
$ maquina="lab01"

$ echo "Maquina: $maquina"     # " " expande variáveis
Maquina: lab01

$ echo 'Maquina: $maquina'     # ' ' é 100% literal
Maquina: $maquina

$ texto="um    dois"
$ echo "$texto"                # preserva os espaços
um    dois
$ echo $texto                  # o shell colapsa: 2 argumentos
um dois
Regra sem exceção prática: toda expansão de variável vai entre aspas duplas — "$var", "$@", "$(comando)". Só deixe sem aspas quando você quer que o resultado vire vários argumentos, e nesse caso escreva um comentário dizendo isso. É a regra que o shellcheck mais cobra (aviso SC2086), e ela sozinha elimina a maioria dos bugs de script.

Passo 10 — Expansões com valor padrão

$ unset destino

# :- usa o padrão SEM alterar a variável
$ echo "${destino:-/tmp/backup}"
/tmp/backup
$ echo "[${destino}]"
[]                    # continua vazia

# := usa o padrão E grava na variável
$ echo "${destino:=/tmp/backup}"
/tmp/backup
$ echo "[${destino}]"
[/tmp/backup]      # agora ficou

# :? aborta com mensagem se estiver vazia — ótimo para validar
$ unset obrigatoria
$ echo "${obrigatoria:?defina a variavel antes de rodar}"
bash: obrigatoria: defina a variavel antes de rodar

Passo 11 — Expansões que manipulam o texto

$ caminho="/home/aluno/oficina/dados/maquinas.csv"

$ echo "${#caminho}"          # tamanho em caracteres
38

$ echo "${caminho##*/}"       # tira o maior prefixo até a última / = basename
maquinas.csv

$ echo "${caminho%/*}"        # tira o menor sufixo a partir da última / = dirname
/home/aluno/oficina/dados

$ arq="${caminho##*/}"
$ echo "${arq%.csv}"          # tira a extensão
maquinas

$ echo "${caminho/aluno/tecnico}"   # troca a PRIMEIRA ocorrência
/home/tecnico/oficina/dados/maquinas.csv

$ echo "${caminho//\//-}"     # // troca TODAS as ocorrências
-home-aluno-oficina-dados-maquinas.csv

$ echo "${arq^^}"  "${arq,,}" # maiúsculas / minúsculas (Bash 4+)
MAQUINAS.CSV maquinas.csv
# corta pela esquerda, % corta pela direita; dobrar o símbolo (##, %%) torna o corte guloso. Um jeito de lembrar: no teclado, # fica à esquerda do %. Essas expansões substituem basename e dirname sem criar processo nenhum.

Passo 12 — Substituição de comando e aritmética

# $( ) captura a SAÍDA de um comando em uma variável
$ hoje="$(date '+%Y-%m-%d')"
$ echo "$hoje"
2026-09-15

$ total_arquivos="$(ls /etc | wc -l)"
$ echo "O /etc tem $total_arquivos entradas"
O /etc tem 231 entradas

# $(( )) faz aritmética inteira
$ maquinas=6
$ por_setor=$(( maquinas / 3 ))
$ echo "$por_setor maquinas por setor"
2 maquinas por setor

$ echo $(( maquinas * 2 + 1 ))
13
$ echo $(( 7 / 2 )) e o resto $(( 7 % 2 ))
3 e o resto 1       # divisão INTEIRA — Bash não faz float

# Incremento dentro de (( )) — sem $ nos nomes
$ contador=0
$ (( contador++ )); (( contador += 5 ))
$ echo "$contador"
6
Bash não tem números decimais. echo $(( 10 / 4 )) devolve 2, não 2.5. Quando precisar de fração, use awk 'BEGIN{print 10/4}' ou bc -l. E cuidado com (( contador++ )) quando contador vale 0: a expressão avalia para 0, ou seja, exit code 1 — o que derruba um script sob set -e. Prefira contador=$(( contador + 1 )) nesses casos.

Passo 13 — Arrays

$ setores=("rede" "suporte" "ensino")

$ echo "${setores[0]}"        # índice começa em 0
rede
$ echo "${setores[@]}"        # todos os elementos
rede suporte ensino
$ echo "${#setores[@]}"       # quantidade de elementos
3
$ echo "${!setores[@]}"       # os índices
0 1 2

# Acrescentar ao final
$ setores+=("laboratorio")
$ echo "${#setores[@]}"
4

# Preencher a partir da saída de um comando
$ mapfile -t usuarios < <(cut -d: -f1 /etc/passwd | head -3)
$ printf '%s\n' "${usuarios[@]}"
root
daemon
bin
"${setores[@]}" com aspas preserva elementos que contêm espaço, um argumento por elemento. Já "${setores[*]}" junta tudo em uma string. Em 99% dos casos você quer [@] entre aspas — a mesma lógica de "$@" que aparece no passo 18.

Passo 14 — Variáveis de ambiente e o arquivo de dados

# Variável de shell: só existe no shell atual
$ local_lab="Bit Livre"
$ bash -c 'echo "[$local_lab]"'
[]                    # o processo filho não enxergou

# export promove a variável a variável de AMBIENTE: filhos herdam
$ export local_lab
$ bash -c 'echo "[$local_lab]"'
[Bit Livre]

$ env | grep local_lab
local_lab=Bit Livre
Agora crie os dados que o resto do exercício vai usar
$ nano dados/maquinas.csv
host,setor,memoria_gb,disco_gb,responsavel
lab01,ensino,8,256,ana
lab02,ensino,16,512,ana
lab03,suporte,8,256,bruno
lab04,suporte,32,1024,bruno
lab05,rede,16,512,carla
lab06,rede,8,128,carla
$ printf '%s\n' ensino suporte rede > dados/setores.txt

$ cat dados/setores.txt
ensino
suporte
rede

# 7 linhas: 1 de cabeçalho + 6 máquinas
$ wc -l < dados/maquinas.csv
7
00:45 – 01:15 Parte C — Argumentos e decisões (passos 15 a 23) 30 min

Um script que só serve para um caso não é automação — é um echo comprido. O que transforma um arquivo em ferramenta é ele receber argumentos, validá-los e decidir o que fazer com eles.

Passo 15 — Os argumentos que todo script recebe

$ nano /tmp/args.sh
#!/usr/bin/env bash
echo "Nome do script (\$0): $0"
echo "Primeiro argumento (\$1): $1"
echo "Segundo argumento (\$2): $2"
echo "Quantidade (\$#): $#"
echo "Todos (\$@): $@"
echo "PID do script (\$\$): $$"
$ bash /tmp/args.sh lab01 ensino
Nome do script ($0): /tmp/args.sh
Primeiro argumento ($1): lab01
Segundo argumento ($2): ensino
Quantidade ($#): 2
Todos ($@): lab01 ensino
PID do script ($$): 48213

# Sem argumentos: as variáveis existem, mas estão vazias
$ bash /tmp/args.sh
Nome do script ($0): /tmp/args.sh
Primeiro argumento ($1): 
Segundo argumento ($2): 
Quantidade ($#): 0
Todos ($@): 
PID do script ($$): 48219
"$@" × "$*": com aspas, "$@" vira N argumentos separados, preservando espaços dentro de cada um; "$*" vira uma única string. Quando você repassa argumentos para outro comando, é sempre "$@".

Passo 16 — Validar antes de agir (erro proposital)

# Sem validação, o script segue com variável vazia e faz besteira em silêncio
$ nano /tmp/ruim.sh
#!/usr/bin/env bash
alvo="$1"
echo "Vou processar: /home/aluno/$alvo"
$ bash /tmp/ruim.sh
Vou processar: /home/aluno/   # o home INTEIRO. Imagine com rm -rf.

# A versão correta valida e sai com código != 0
$ nano /tmp/bom.sh
#!/usr/bin/env bash
if [[ $# -lt 1 ]]; then
    echo "Uso: $0 <host>" >&2
    exit 1
fi
echo "Vou processar: /home/aluno/$1"
$ bash /tmp/bom.sh; echo "codigo=$?"
Uso: /tmp/bom.sh <host>
codigo=1

$ bash /tmp/bom.sh lab01; echo "codigo=$?"
Vou processar: /home/aluno/lab01
codigo=0

# A mensagem de uso foi para o stderr — prove descartando o stdout
$ bash /tmp/bom.sh >/dev/null
Uso: /tmp/bom.sh <host>   # continuou aparecendo: veio do fd 2
Mensagem de erro vai para o stderr (>&2) e script que falhou sai com código diferente de zero. Sem essas duas coisas, quem chamar o seu script — outro script, o cron, um pipeline de CI — não tem como saber que deu errado.

Passo 17 — shift e o primeiro script útil da oficina

$ nano bin/ficha.sh
Conteúdo de bin/ficha.sh
#!/usr/bin/env bash
#
# ficha.sh — gera a ficha de uma máquina em fichas/<host>.txt
# Uso: ./bin/ficha.sh <host> [observacao...]
#
set -euo pipefail

if [[ $# -lt 1 ]]; then
    echo "Uso: $0 <host> [observacao...]" >&2
    exit 1
fi

host="$1"
shift                       # descarta o $1; o que sobrou vira a observação
observacao="${*:-sem observacoes}"

csv="$HOME/oficina/dados/maquinas.csv"
destino="$HOME/oficina/fichas/${host}.txt"

linha=$(grep "^${host}," "$csv") || {
    echo "ERRO: host '$host' nao existe em $csv" >&2
    exit 2
}

setor=$(echo "$linha" | cut -d, -f2)
memoria=$(echo "$linha" | cut -d, -f3)
disco=$(echo "$linha" | cut -d, -f4)
dono=$(echo "$linha" | cut -d, -f5)

{
    echo "host=$host"
    echo "setor=$setor"
    echo "memoria=${memoria}G"
    echo "disco=${disco}G"
    echo "responsavel=$dono"
    echo "gerado_em=$(date '+%Y-%m-%d %H:%M')"
    echo "obs=$observacao"
} > "$destino"

echo "Ficha gravada em $destino"
$ chmod +x bin/ficha.sh
$ ./bin/ficha.sh lab01
Ficha gravada em /home/aluno/oficina/fichas/lab01.txt

$ cat fichas/lab01.txt
host=lab01
setor=ensino
memoria=8G
disco=256G
responsavel=ana
gerado_em=2026-09-15 10:41
obs=sem observacoes

# Agora com observação de várias palavras — o shift em ação
$ ./bin/ficha.sh lab04 trocar fonte em outubro
$ tail -1 fichas/lab04.txt
obs=trocar fonte em outubro

# E o caminho de erro
$ ./bin/ficha.sh lab99; echo "codigo=$?"
ERRO: host 'lab99' nao existe em /home/aluno/oficina/dados/maquinas.csv
codigo=2
Repare em três técnicas novas: o agrupamento { ... } > arquivo, que redireciona um bloco inteiro de echo com um único >; o || { ...; exit 2; }, que trata a falha do grep sem if; e ${*:-sem observacoes}, que combina "todos os argumentos restantes" com um valor padrão.

Passo 18 — getopts: flags de verdade

$ nano /tmp/opts.sh
#!/usr/bin/env bash
set -euo pipefail

setor="todos"
verbose=0
saida=""

while getopts ":s:o:vh" opt; do
    case "$opt" in
        s) setor="$OPTARG" ;;
        o) saida="$OPTARG" ;;
        v) verbose=1 ;;
        h) echo "Uso: $0 [-s setor] [-o arquivo] [-v]"; exit 0 ;;
        \?) echo "Opcao invalida: -$OPTARG" >&2; exit 1 ;;
        :)  echo "A opcao -$OPTARG exige um valor" >&2; exit 1 ;;
    esac
done
shift $(( OPTIND - 1 ))    # descarta as flags já processadas

echo "setor=$setor verbose=$verbose saida=${saida:-stdout}"
echo "argumentos restantes: $# -> $*"
$ bash /tmp/opts.sh -s rede -v extra1 extra2
setor=rede verbose=1 saida=stdout
argumentos restantes: 2 -> extra1 extra2

$ bash /tmp/opts.sh -z
Opcao invalida: -z
$ bash /tmp/opts.sh -s
A opcao -s exige um valor
Os dois-pontos têm dois papéis diferentes na string ":s:o:vh": o primeiro, no início, liga o modo silencioso — é ele que faz os casos \? e : existirem em vez de o Bash imprimir a própria mensagem. Os demais, depois de uma letra, dizem que aquela flag exige um valor, que chega em $OPTARG. O shift $(( OPTIND - 1 )) no final é obrigatório: sem ele, "$@" ainda contém as flags.

Passo 19 — Testes de string

$ a="lab01"; b="lab02"; vazia=""

$ [[ "$a" == "$b" ]]; echo $?
1                     # diferentes → falso
$ [[ "$a" != "$b" ]]; echo $?
0
$ [[ -z "$vazia" ]]; echo $?
0                     # -z: zero-length, string vazia
$ [[ -n "$a" ]]; echo $?
0                     # -n: não vazia

# Casamento de padrão (glob) — só funciona em [[ ]], e sem aspas no padrão
$ [[ "$a" == lab* ]]; echo $?
0
# Expressão regular com =~ ; o casamento fica em BASH_REMATCH
$ [[ "$a" =~ ^lab([0-9]+)$ ]] && echo "numero = ${BASH_REMATCH[1]}"
numero = 01

Passo 20 — Testes numéricos

$ mem=16

$ [[ $mem -gt 8 ]]  && echo "maior que 8"
maior que 8
$ [[ $mem -eq 16 ]] && echo "igual a 16"
igual a 16
$ [[ $mem -le 32 ]] && echo "menor ou igual a 32"
menor ou igual a 32

# Forma aritmética (( )) — mais legível, aceita < > == e dispensa o $
$ (( mem > 8 )) && echo "idem, com (( ))"
idem, com (( ))
ComparaçãoNúmerosStrings
igual-eq==
diferente-ne!=
maior-gt> (ordem alfabética)
maior ou igual-ge
menor-lt< (ordem alfabética)
menor ou igual-le
Trocar os dois conjuntos é o erro clássico: [[ "10" > "9" ]] é falso, porque compara texto — e "1" vem antes de "9" no alfabeto. Para número, use -gt ou (( )).

Passo 21 — Testes de arquivo e diretório (erro proposital)

# O teste mais comum de todos, escrito do jeito errado
$ arq="/tmp/nao existe.txt"
$ [ -f $arq ]; echo $?
bash: [: /tmp/nao: binary operator expected

# Com aspas (e com [[ ]], que é mais tolerante) funciona
$ [[ -f "$arq" ]]; echo $?
1

$ [[ -f dados/maquinas.csv ]] && echo "existe e é arquivo comum"
existe e é arquivo comum
$ [[ -d fichas ]]            && echo "existe e é diretório"
existe e é diretório
$ [[ -s dados/maquinas.csv ]] && echo "existe e NÃO está vazio"
existe e NÃO está vazio
$ [[ -r dados/maquinas.csv ]] && echo "posso ler"
posso ler
$ [[ -x bin/ficha.sh ]]       && echo "posso executar"
posso executar
$ [[ ! -e /tmp/inexistente ]] && echo "! nega: realmente não existe"
! nega: realmente não existe

# Combinando: && = E lógico, || = OU lógico
$ [[ -f dados/maquinas.csv && -s dados/maquinas.csv ]] && echo "arquivo válido"
arquivo válido
Prefira [[ ]] a [ ] em Bash: ele não sofre word splitting dentro do teste, aceita &&/||/=~ e não exige aspas em todo canto. Use [ ] só quando o script precisar rodar em /bin/sh POSIX puro.

Passo 22 — if / elif / else

$ nano /tmp/classifica.sh
#!/usr/bin/env bash
mem="${1:-0}"

if (( mem >= 32 )); then
    echo "servidor"
elif (( mem >= 16 )); then
    echo "estacao"
elif (( mem >= 4 )); then
    echo "basica"
else
    echo "insuficiente"
fi
$ for m in 64 16 8 2; do printf '%3s -> ' "$m"; bash /tmp/classifica.sh "$m"; done
 64 -> servidor
 16 -> estacao
  8 -> basica
  2 -> insuficiente

Passo 23 — case e o segundo script da oficina

$ nano bin/busca.sh
Conteúdo de bin/busca.sh
#!/usr/bin/env bash
#
# busca.sh — lista as máquinas de um setor
# Uso: ./bin/busca.sh {ensino|suporte|rede|todos}
#
set -euo pipefail

csv="$HOME/oficina/dados/maquinas.csv"
alvo="${1:-}"

case "$alvo" in
    ensino|suporte|rede)
        echo "== Setor: $alvo =="
        grep ",${alvo}," "$csv" | cut -d, -f1,3,5
        ;;
    todos|"")
        echo "== Todas as maquinas =="
        tail -n +2 "$csv" | cut -d, -f1,2,5
        ;;
    -h|--help)
        echo "Uso: $0 {ensino|suporte|rede|todos}"
        exit 0
        ;;
    *)
        echo "Setor desconhecido: '$alvo'" >&2
        echo "Validos: ensino, suporte, rede, todos" >&2
        exit 1
        ;;
esac
$ chmod +x bin/busca.sh

$ ./bin/busca.sh rede
== Setor: rede ==
lab05,16,carla
lab06,8,carla

$ ./bin/busca.sh
== Todas as maquinas ==
lab01,ensino,ana
lab02,ensino,ana
lab03,suporte,bruno
lab04,suporte,bruno
lab05,rede,carla
lab06,rede,carla

$ ./bin/busca.sh financeiro; echo "codigo=$?"
Setor desconhecido: 'financeiro'
Validos: ensino, suporte, rede, todos
codigo=1
O case compara padrões glob, não regex: ensino|suporte|rede) aceita qualquer um dos três, lab*) aceitaria qualquer coisa começando com lab, e *) no final é o default. Cada ramo termina em ;;, e a ordem importa: o primeiro padrão que casar vence. Note também tail -n +2, que pula o cabeçalho do CSV.
01:15 – 01:40 Parte D — Repetição (passos 24 a 30) 25 min

Loop é onde o script deixa de economizar digitação e passa a economizar tempo. Seis máquinas ainda dá para fazer à mão; sessenta, não. Escreva o loop uma vez e o número deixa de importar.

Passo 24 — for sobre uma lista

$ for setor in ensino suporte rede; do
>     echo "Processando setor: $setor"
> done
Processando setor: ensino
Processando setor: suporte
Processando setor: rede

# Sobre um array
$ hosts=(lab01 lab02 lab03)
$ for h in "${hosts[@]}"; do echo "-> $h"; done
-> lab01
-> lab02
-> lab03

# Sobre a saída de um comando
$ for s in $(cat dados/setores.txt); do
>     echo "$s: $(grep -c ",${s}," dados/maquinas.csv) maquinas"
> done
ensino: 2 maquinas
suporte: 2 maquinas
rede: 2 maquinas

Passo 25 — for sobre arquivos (glob), do jeito seguro

# Gere primeiro as fichas que faltam
$ for h in lab02 lab03 lab05 lab06; do ./bin/ficha.sh "$h" >/dev/null; done
$ ls fichas
lab01.txt  lab02.txt  lab03.txt  lab04.txt  lab05.txt  lab06.txt

# O glob é expandido pelo SHELL antes do loop começar
$ for f in fichas/*.txt; do
>     echo "$(basename "$f"): $(wc -l < "$f") linhas"
> done
lab01.txt: 7 linhas
lab02.txt: 7 linhas
lab03.txt: 7 linhas
lab04.txt: 7 linhas
lab05.txt: 7 linhas
lab06.txt: 7 linhas
Nunca escreva for f in $(ls *.txt). O $(ls) devolve uma string que o shell divide nos espaços, então relatorio de setembro.txt viraria três iterações. O glob for f in *.txt não tem esse problema — cada arquivo é um item, com espaços e tudo. Se o diretório estiver vazio, o glob não expande e $f vem com o padrão literal; proteja com [[ -e "$f" ]] || continue.

Passo 26 — for com contadores

# Estilo C — quando você precisa do índice
$ for (( i=1; i<=3; i++ )); do echo "tentativa $i"; done
tentativa 1
tentativa 2
tentativa 3

# Brace expansion — literal, expandida antes de tudo
$ echo lab{01..06}
lab01 lab02 lab03 lab04 lab05 lab06
$ for h in lab{01..06}; do printf '%s ' "$h"; done; echo
lab01 lab02 lab03 lab04 lab05 lab06 

# seq — quando os limites vêm de variáveis (brace expansion NÃO expande variável)
$ n=4
$ echo {1..$n}
{1..4}              # não expandiu: é uma armadilha conhecida
$ for i in $(seq 1 "$n"); do printf '%s ' "$i"; done; echo
1 2 3 4 

Passo 27 — while com condição

$ contador=1
$ while (( contador <= 3 )); do
>     echo "contador = $contador"
>     contador=$(( contador + 1 ))
> done
contador = 1
contador = 2
contador = 3
Retentativa com espera — o padrão mais útil do while
$ nano /tmp/espera.sh
#!/usr/bin/env bash
alvo="${1:-/tmp/sinal.txt}"
tentativa=1
max=5

while [[ ! -f "$alvo" ]] && (( tentativa <= max )); do
    echo "[$tentativa/$max] aguardando $alvo ..."
    sleep 1
    tentativa=$(( tentativa + 1 ))
done

if [[ -f "$alvo" ]]; then
    echo "Apareceu!"
else
    echo "Desisti apos $max tentativas" >&2
    exit 1
fi
$ rm -f /tmp/sinal.txt
$ bash /tmp/espera.sh; echo "codigo=$?"
[1/5] aguardando /tmp/sinal.txt ...
[2/5] aguardando /tmp/sinal.txt ...
[3/5] aguardando /tmp/sinal.txt ...
[4/5] aguardando /tmp/sinal.txt ...
[5/5] aguardando /tmp/sinal.txt ...
Desisti apos 5 tentativas
codigo=1

# Agora crie o arquivo e rode de novo
$ touch /tmp/sinal.txt
$ bash /tmp/espera.sh; echo "codigo=$?"
Apareceu!
codigo=0

Passo 28 — Ler um arquivo linha a linha (o jeito certo)

$ while IFS=, read -r host setor mem disco dono; do
>     echo "$host ($setor) — ${mem}GB — resp.: $dono"
> done < <(tail -n +2 dados/maquinas.csv)
lab01 (ensino) — 8GB — resp.: ana
lab02 (ensino) — 16GB — resp.: ana
lab03 (suporte) — 8GB — resp.: bruno
lab04 (suporte) — 32GB — resp.: bruno
lab05 (rede) — 16GB — resp.: carla
lab06 (rede) — 8GB — resp.: carla
Três detalhes que fazem essa linha ser a forma canônica: IFS=, troca o separador só para este read, o que quebra o CSV em campos; -r impede que o read interprete \ como escape; e o < <(...)process substitution — alimenta o loop sem criar subshell, então variáveis alteradas dentro dele sobrevivem depois do done. Com cat arquivo | while ... isso não aconteceria: o pipe põe o loop em um subshell e você perde tudo.
Prove o problema do pipe
$ total=0
$ cat dados/setores.txt | while read -r s; do total=$(( total + 1 )); done
$ echo "$total"
0                     # o loop rodou em um subshell — a soma se perdeu

$ total=0
$ while read -r s; do total=$(( total + 1 )); done < dados/setores.txt
$ echo "$total"
3                     # redirecionamento: mesmo shell, valor preservado

Passo 29 — until: o while ao contrário

$ n=5
$ until (( n == 0 )); do
>     echo "faltam $n"
>     n=$(( n - 1 ))
> done
faltam 5
faltam 4
faltam 3
faltam 2
faltam 1

# while roda ENQUANTO é verdade; until roda ATÉ virar verdade
$ rm -f /tmp/pronto
$ ( sleep 2; touch /tmp/pronto ) &
$ until [[ -f /tmp/pronto ]]; do echo "ainda nao..."; sleep 1; done; echo "pronto!"
ainda nao...
ainda nao...
pronto!

Passo 30 — break, continue e um contador real

$ nano /tmp/conta.sh
#!/usr/bin/env bash
set -euo pipefail

csv="$HOME/oficina/dados/maquinas.csv"
total_mem=0
grandes=0

while IFS=, read -r host setor mem disco dono; do
    # continue: pula o resto DESTA volta
    [[ "$setor" == "rede" ]] && continue

    total_mem=$(( total_mem + mem ))
    (( mem >= 16 )) && grandes=$(( grandes + 1 ))

    # break: abandona o loop inteiro
    if (( total_mem > 100 )); then
        echo "Limite de 100GB estourado em $host — parando"
        break
    fi
done < <(tail -n +2 "$csv")

echo "Memoria somada (sem o setor rede): ${total_mem}GB"
echo "Maquinas com 16GB ou mais: $grandes"
$ bash /tmp/conta.sh
Memoria somada (sem o setor rede): 64GB
Maquinas com 16GB ou mais: 2
Confira a conta na mão: lab01 (8) + lab02 (16) + lab03 (8) + lab04 (32) = 64. As duas máquinas do setor rede foram puladas pelo continue, e o break nunca disparou porque 64 < 100. Troque o limite para 50 e rode de novo para ver o break em ação.
01:40 – 02:10 Parte E — Funções e I/O (passos 31 a 38) 30 min

Aqui o script vira software. Função é o que permite escrever uma vez e usar em todo canto; redirecionamento é o que permite que dois scripts conversem sem inventar formato de arquivo. Os dois juntos são a base de qualquer automação séria.

Passo 31 — Declarar e chamar uma função

$ nano /tmp/func.sh
#!/usr/bin/env bash

saudacao() {
    echo "Oficina Bit Livre"
}

# Função recebe argumentos como o script: $1, $2, $#, "$@"
apresentar() {
    echo "Maquina: $1 | Setor: $2 | (recebi $# argumentos)"
}

saudacao
apresentar lab01 ensino
apresentar lab05 rede
$ bash /tmp/func.sh
Oficina Bit Livre
Maquina: lab01 | Setor: ensino | (recebi 2 argumentos)
Maquina: lab05 | Setor: rede | (recebi 2 argumentos)
A função precisa estar definida antes de ser chamada — o Bash lê o arquivo de cima para baixo. Por isso o padrão é: cabeçalho, funções, e só no final o "corpo" do script. E o $1 dentro de uma função é o argumento dela, não o do script.

Passo 32 — local e "retorno" pelo stdout

$ nano /tmp/local.sh
#!/usr/bin/env bash

nome="global"

sem_local() {
    nome="mudei tudo"      # atropela a variável do script
}

com_local() {
    local nome="só aqui dentro"
    echo "dentro da funcao: $nome"
}

# "Retornar" um dado = escrever no stdout e capturar com $( )
somar() {
    local a="$1" b="$2"
    echo $(( a + b ))
}

com_local
echo "depois de com_local:  $nome"
sem_local
echo "depois de sem_local:  $nome"

total=$(somar 8 16)
echo "8 + 16 = $total"
$ bash /tmp/local.sh
dentro da funcao: só aqui dentro
depois de com_local:  global
depois de sem_local:  mudei tudo
8 + 16 = 24
Toda variável de função leva local. Sem isso, ela é global e um nome comum como i, arquivo ou tmp vai colidir com o do script principal — bug silencioso e difícil de achar.

Passo 33 — return devolve status, não dado

$ nano /tmp/status.sh
#!/usr/bin/env bash

# O corpo é um único teste: seu exit code vira o da função
host_existe() {
    grep -q "^${1}," "$HOME/oficina/dados/maquinas.csv"
}

# Ou explicitamente com return
tem_memoria_suficiente() {
    local mem="$1"
    if (( mem >= 16 )); then
        return 0        # 0 = sucesso/verdadeiro
    fi
    return 1            # != 0 = falha/falso
}

for h in lab01 lab99; do
    if host_existe "$h"; then
        echo "$h: cadastrado"
    else
        echo "$h: NAO cadastrado" >&2
    fi
done

tem_memoria_suficiente 32 && echo "32GB: ok"
tem_memoria_suficiente 4  || echo "4GB: insuficiente"
$ bash /tmp/status.sh
lab01: cadastrado
lab99: NAO cadastrado
32GB: ok
4GB: insuficiente
return 5 não devolve o número 5 para x=$(funcao) — devolve o exit code 5, que você lê em $?. Dado sai por echo; status sai por return. Misturar os dois — mandar log para o stdout de uma função cujo valor você captura — é o bug clássico. Log vai para o stderr (>&2).

Passo 34 — A biblioteca de funções da oficina

$ nano bin/lib-log.sh
Conteúdo de bin/lib-log.sh — note que NÃO tem shebang e NÃO leva chmod +x
# lib-log.sh — funções de log da oficina
# Carregue com:  source "$HOME/oficina/bin/lib-log.sh"

LOG_FILE="${LOG_FILE:-$HOME/oficina/logs/oficina.log}"

log() {
    local nivel="$1"; shift
    printf '[%s] [%-5s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$nivel" "$*" \
        | tee -a "$LOG_FILE"
}

log_info()  { log INFO  "$@"; }
log_warn()  { log WARN  "$@" >&2; }
log_error() { log ERROR "$@" >&2; }

die() {
    log_error "$*"
    exit 1
}
# Biblioteca não se executa — se carrega. Confirme que ela NÃO é executável:
$ ls -l bin/lib-log.sh
-rw-rw-r-- 1 aluno aluno 421 Sep 15 11:22 bin/lib-log.sh

$ source bin/lib-log.sh
$ log_info "biblioteca carregada"
[2026-09-15 11:23:04] [INFO ] biblioteca carregada
$ log_warn "disco do lab06 pequeno"
[2026-09-15 11:23:11] [WARN ] disco do lab06 pequeno

# O tee -a gravou tudo no arquivo E mostrou na tela
$ cat logs/oficina.log
[2026-09-15 11:23:04] [INFO ] biblioteca carregada
[2026-09-15 11:23:11] [WARN ] disco do lab06 pequeno

# Prove que log_warn foi para o stderr: descarte o stdout
$ log_info "isto some" >/dev/null
$ log_warn "isto aparece" >/dev/null
[2026-09-15 11:23:40] [WARN ] isto aparece

Passo 35 — Redirecionamento e a ordem que engana todo mundo

# Um comando que escreve nos dois streams ao mesmo tempo
$ ls dados /pasta-inexistente
ls: cannot access '/pasta-inexistente': No such file or directory
dados:
maquinas.csv
setores.txt

# Só o stdout no arquivo; o erro continua na tela
$ ls dados /pasta-inexistente > /tmp/saida.txt
ls: cannot access '/pasta-inexistente': No such file or directory

# Só o stderr no arquivo; o resto na tela
$ ls dados /pasta-inexistente 2> /tmp/erros.txt
dados:
maquinas.csv
setores.txt
$ cat /tmp/erros.txt
ls: cannot access '/pasta-inexistente': No such file or directory

# CERTO: 2>&1 por ÚLTIMO — os dois vão para o arquivo
$ ls dados /pasta-inexistente > /tmp/tudo.txt 2>&1
$ wc -l < /tmp/tudo.txt
4

# ERRADO: 2>&1 antes — o stderr foi apontado para o TERMINAL, não para o arquivo
$ ls dados /pasta-inexistente 2>&1 > /tmp/parcial.txt
ls: cannot access '/pasta-inexistente': No such file or directory
$ wc -l < /tmp/parcial.txt
3                     # faltou a linha de erro
O shell processa redirecionamentos da esquerda para a direita, e 2>&1 significa "aponte o fd 2 para onde o fd 1 aponta agora" — não "junte os dois para sempre". Em >arq 2>&1 o fd 1 já foi para o arquivo quando o fd 2 o copia: ambos no arquivo. Em 2>&1 >arq o fd 2 copia o terminal e só depois o fd 1 muda: metade perdida. O atalho &> arq é do Bash e faz o certo, mas > arq 2>&1 funciona também em /bin/sh.

Passo 36 — /dev/null, tee e append

# Silenciar completamente um comando
$ grep -q pinguim /etc/passwd >/dev/null 2>&1; echo "codigo=$?"
codigo=1       # nada na tela, mas o exit code continua valendo

# > sobrescreve, >> acrescenta
$ echo "primeira"  >  /tmp/lista.txt
$ echo "segunda"   >> /tmp/lista.txt
$ echo "terceira"  >> /tmp/lista.txt
$ cat /tmp/lista.txt
primeira
segunda
terceira

$ echo "recomecei" > /tmp/lista.txt
$ cat /tmp/lista.txt
recomecei       # o > apagou tudo sem perguntar

# tee bifurca: grava E deixa passar adiante no pipe
$ ./bin/busca.sh todos | tee /tmp/todas.txt | grep rede
lab05,rede,carla
lab06,rede,carla
$ wc -l < /tmp/todas.txt
7              # o arquivo guardou a saída COMPLETA

Passo 37 — Pipes, xargs e process substitution

# Pipe: a saída de um vira a entrada do próximo
$ tail -n +2 dados/maquinas.csv | cut -d, -f2 | sort | uniq -c
      2 ensino
      2 rede
      2 suporte

# Somar uma coluna com awk
$ tail -n +2 dados/maquinas.csv | awk -F, '{s+=$3} END {print s" GB no total"}'
88 GB no total

# xargs: transforma linhas da entrada em ARGUMENTOS de um comando
$ ls fichas/*.txt | xargs wc -l | tail -1
  42 total

# Process substitution: trata a saída de um comando como se fosse um arquivo
$ diff <(./bin/busca.sh ensino) <(./bin/busca.sh rede)
1,3c1,3
< == Setor: ensino ==
< lab01,8,ana
< lab02,16,ana
---
> == Setor: rede ==
> lab05,16,carla
> lab06,8,carla

# Sem process substitution, isso exigiria dois arquivos temporários
$ comm -12 <(ls fichas | sort) <(printf '%s\n' lab01.txt lab99.txt | sort)
lab01.txt
<(comando) devolve um caminho como /dev/fd/63 que o outro comando abre como se fosse arquivo comum. É o que torna possível comparar duas saídasdiff, comm e join só sabem ler arquivos.

Passo 38 — Here-document e here-string

# Delimitador SEM aspas: expande $variaveis e $(comandos)
$ setor="rede"
$ cat <<EOF > /tmp/relatorio-cabecalho.txt
> Relatorio da Oficina Bit Livre
> Setor: $setor
> Gerado em: $(date '+%F')
> EOF
$ cat /tmp/relatorio-cabecalho.txt
Relatorio da Oficina Bit Livre
Setor: rede
Gerado em: 2026-09-15

# Delimitador COM aspas: tudo literal — ideal para gerar scripts e configs
$ cat <<'EOF' > /tmp/modelo.sh
> #!/usr/bin/env bash
> echo "O host e $HOSTNAME e o usuario e $USER"
> EOF
$ cat /tmp/modelo.sh
#!/usr/bin/env bash
echo "O host e $HOSTNAME e o usuario e $USER"   # não expandiu: era isso que queríamos

# Here-string: uma linha só direto no stdin
$ read -r host setor resto <<< "lab05 rede carla"
$ echo "$setor"
rede
$ grep -c ',' <<< "$(tail -n +2 dados/maquinas.csv)"
6
A escolha entre <<EOF e <<'EOF' é a diferença entre um arquivo de configuração preenchido e um modelo. Se o texto contém $ que deve sobreviver — um script gerado, um awk, um systemd unit — o delimitador vai entre aspas.
02:10 – 02:30 Parte F — Robustez e entrega (passos 39 a 43) 20 min

Script que funciona no dia em que foi escrito é fácil. O que separa um script de oficina de um script de produção são quatro linhas de cabeçalho, um trap e a disciplina de passar o shellcheck antes de entregar.

Passo 39 — set -x: ver o que o shell realmente executou

$ nano /tmp/rastro.sh
#!/usr/bin/env bash
set -x                      # liga o rastreamento

host="lab04"
mem=$(grep "^${host}," "$HOME/oficina/dados/maquinas.csv" | cut -d, -f3)
total=$(( mem * 2 ))

set +x                      # desliga
echo "Resultado final: $total"
$ bash /tmp/rastro.sh
+ host=lab04
++ grep '^lab04,' /home/aluno/oficina/dados/maquinas.csv
++ cut -d, -f3
+ mem=32
+ total=64
+ set +x
Resultado final: 64
Cada + é um nível de profundidade: um + é o script, ++ é uma substituição de comando dentro dele. Repare que o rastro mostra o comando já expandido'^lab04,', não "^${host},". É exatamente por isso que set -x resolve bug de quoting em segundos. Para ligar sem editar o arquivo, rode bash -x script.sh.

Passo 40 — set -euo pipefail e o que cada letra evita

# SEM proteção: o script segue depois do erro e apaga o que não devia
$ nano /tmp/frouxo.sh
#!/usr/bin/env bash
cd /pasta-que-nao-existe
echo "Ainda estou vivo em $(pwd) — e vou 'limpar' este diretorio"
echo "Valor de uma variavel nunca definida: [$naodefinida]"
$ bash /tmp/frouxo.sh; echo "codigo=$?"
/tmp/frouxo.sh: line 2: cd: /pasta-que-nao-existe: No such file or directory
Ainda estou vivo em /home/aluno/oficina — e vou 'limpar' este diretorio
Valor de uma variavel nunca definida: []
codigo=0
Leia de novo o codigo=0: para o sistema, esse script deu certo. Ele errou o cd, continuou rodando no diretório errado e ainda assim reportou sucesso. Troque os dois echo por um rm -rf * e você tem o acidente mais famoso da administração de sistemas.
# COM proteção: para na primeira falha
$ nano /tmp/firme.sh
#!/usr/bin/env bash
set -euo pipefail

cd /pasta-que-nao-existe
echo "esta linha nunca roda"
$ bash /tmp/firme.sh; echo "codigo=$?"
/tmp/firme.sh: line 4: cd: /pasta-que-nao-existe: No such file or directory
codigo=1

# -u sozinho: variável não definida vira erro fatal, não string vazia
$ bash -u -c 'echo "[$naodefinida]"'; echo "codigo=$?"
bash: line 1: naodefinida: unbound variable
codigo=1

# pipefail: sem ele, o pipe reporta só o ÚLTIMO comando
$ bash -c 'false | true'; echo "sem pipefail: $?"
sem pipefail: 0
$ bash -c 'set -o pipefail; false | true'; echo "com pipefail: $?"
com pipefail: 1
OpçãoO que fazO acidente que evita
set -e Aborta na primeira falha (exit code != 0) Continuar processando depois de um cd, mkdir ou tar que falhou
set -u Erro ao usar variável não definida rm -rf "$BASE/$SUB" virar rm -rf / por causa de um nome digitado errado
set -o pipefail O pipe falha se qualquer etapa falhar gera_backup | gzip > f.gz reportar sucesso com o backup corrompido
set -x Imprime cada comando já expandido Horas de echo espalhado pelo script para achar um bug de aspas
Com set -e, um comando que você espera que possa falhar precisa ser neutralizado — comando || true — ou usado dentro de um if. Do contrário o script morre justamente onde você já tinha previsto a falha.

Passo 41 — trap: limpeza garantida em qualquer saída

$ nano /tmp/limpo.sh
#!/usr/bin/env bash
set -euo pipefail

TMPDIR="$(mktemp -d)"
echo "Temporario criado: $TMPDIR"

limpar() {
    rm -rf "$TMPDIR"
    echo "[trap] $TMPDIR removido"
}
trap limpar EXIT                                   # dispara em QUALQUER saída
trap 'echo "[trap] interrompido pelo usuario"; exit 130' INT

echo "trabalhando..." > "$TMPDIR/dados.txt"
ls "$TMPDIR"

if [[ "${1:-}" == "falhar" ]]; then
    echo "vou falhar de proposito" >&2
    exit 7
fi

echo "terminei normalmente"
# Saída normal: o trap rodou
$ bash /tmp/limpo.sh; echo "codigo=$?"
Temporario criado: /tmp/tmp.9KpQ2xVdLm
dados.txt
terminei normalmente
[trap] /tmp/tmp.9KpQ2xVdLm removido
codigo=0

# Saída por erro: o trap rodou IGUAL — é essa a graça
$ bash /tmp/limpo.sh falhar; echo "codigo=$?"
Temporario criado: /tmp/tmp.Xa7Fn2bQrT
dados.txt
vou falhar de proposito
[trap] /tmp/tmp.Xa7Fn2bQrT removido
codigo=7

# Nenhum temporário ficou órfão
$ ls -d /tmp/tmp.* 2>/dev/null | wc -l
0
O pseudo-sinal EXIT dispara em toda forma de término: fim normal, exit explícito, erro sob set -e e — se você também tratar INTCtrl+C. Registre o trap logo depois de criar o recurso, nunca antes. É o finally do shell.

Passo 42 — shellcheck: o revisor que nunca cansa

$ shellcheck --version 2>/dev/null || sudo apt install -y shellcheck
# Um script com os quatro erros mais comuns do mundo
$ nano /tmp/torto.sh
#!/usr/bin/env bash
arquivo=$1
if [ $arquivo == "lab01" ]; then
    echo "achei"
fi
for f in $(ls *.txt); do
    echo $f
done
$ shellcheck /tmp/torto.sh
In /tmp/torto.sh line 2:
arquivo=$1
        ^-- SC2086: Double quote to prevent globbing and word splitting.

In /tmp/torto.sh line 3:
if [ $arquivo == "lab01" ]; then
     ^------^ SC2086: Double quote to prevent globbing and word splitting.

In /tmp/torto.sh line 6:
for f in $(ls *.txt); do
         ^---------^ SC2045: Iterating over ls output is fragile. Use globs.

In /tmp/torto.sh line 7:
    echo $f
         ^-- SC2086: Double quote to prevent globbing and word splitting.

# Agora rode nos SEUS scripts — o objetivo é saída vazia
$ shellcheck bin/*.sh; echo "codigo=$?"
codigo=0
Saída vazia e codigo=0 é a meta. Se aparecer algum SC2086 nos seus scripts, é aspas faltando — volte ao passo 9. O shellcheck encontra em um segundo o que levaria uma tarde de depuração, e é ele que corrige o vício de escrever $var sem aspas.

Passo 43 — O script integrador: relatorio.sh

Este é o entregável do roteiro. Ele usa tudo: cabeçalho defensivo, trap, biblioteca com source, getopts, validação, while read, case, aritmética, função com local, here-document e redirecionamento.

$ nano bin/relatorio.sh
Conteúdo de bin/relatorio.sh
#!/usr/bin/env bash
#
# relatorio.sh — relatorio de inventario da Oficina Bit Livre
# Uso: ./bin/relatorio.sh [-s setor] [-m memoria_minima] [-o arquivo]
#
set -euo pipefail

BASE="$HOME/oficina"
CSV="$BASE/dados/maquinas.csv"
source "$BASE/bin/lib-log.sh"

TMP="$(mktemp)"
trap 'rm -f "$TMP"' EXIT

setor="todos"
mem_min=0
saida=""

while getopts ":s:m:o:h" opt; do
    case "$opt" in
        s) setor="$OPTARG" ;;
        m) mem_min="$OPTARG" ;;
        o) saida="$OPTARG" ;;
        h) echo "Uso: $0 [-s setor] [-m memoria_minima] [-o arquivo]"; exit 0 ;;
        \?) die "Opcao invalida: -$OPTARG" ;;
        :)  die "A opcao -$OPTARG exige um valor" ;;
    esac
done

[[ -r "$CSV" ]] || die "Nao consigo ler $CSV"
[[ "$mem_min" =~ ^[0-9]+$ ]] || die "-m exige um numero inteiro, recebi '$mem_min'"

classificar() {
    local mem="$1"
    if   (( mem >= 32 )); then echo "servidor"
    elif (( mem >= 16 )); then echo "estacao"
    else                     echo "basica"
    fi
}

log_info "relatorio iniciado (setor=$setor mem_min=${mem_min}GB)"

total=0
soma_mem=0
soma_disco=0

while IFS=, read -r host s mem disco dono; do
    case "$setor" in
        todos)   : ;;                                  # : = não faz nada
        "$s")     : ;;
        *)       continue ;;                         # não é o setor pedido
    esac
    (( mem < mem_min )) && continue

    total=$(( total + 1 ))
    soma_mem=$(( soma_mem + mem ))
    soma_disco=$(( soma_disco + disco ))
    printf '%-8s %-9s %4sGB %6sGB  %-8s %s\n' \
        "$host" "$s" "$mem" "$disco" "$dono" "$(classificar "$mem")" >> "$TMP"
done < <(tail -n +2 "$CSV")

(( total > 0 )) || die "nenhuma maquina atende aos filtros"

gerar() {
cat <<EOF
================================================
 OFICINA BIT LIVRE — RELATORIO DE INVENTARIO
 Gerado em : $(date '+%Y-%m-%d %H:%M:%S')
 Filtros   : setor=$setor  memoria_minima=${mem_min}GB
================================================
EOF
cat "$TMP"
cat <<EOF
------------------------------------------------
 Maquinas       : $total
 Memoria total  : ${soma_mem}GB   (media $(( soma_mem / total ))GB)
 Disco total    : ${soma_disco}GB
================================================
EOF
}

if [[ -n "$saida" ]]; then
    gerar > "$saida"
    log_info "relatorio gravado em $saida ($total maquinas)"
else
    gerar
    log_info "relatorio exibido na tela ($total maquinas)"
fi
$ chmod +x bin/relatorio.sh
$ shellcheck bin/relatorio.sh; echo "shellcheck=$?"
shellcheck=0

$ ./bin/relatorio.sh
[2026-09-15 12:04:18] [INFO ] relatorio iniciado (setor=todos mem_min=0GB)
================================================
 OFICINA BIT LIVRE — RELATORIO DE INVENTARIO
 Gerado em : 2026-09-15 12:04:18
 Filtros   : setor=todos  memoria_minima=0GB
================================================
lab01    ensino       8GB    256GB  ana      basica
lab02    ensino      16GB    512GB  ana      estacao
lab03    suporte      8GB    256GB  bruno    basica
lab04    suporte     32GB   1024GB  bruno    servidor
lab05    rede        16GB    512GB  carla    estacao
lab06    rede         8GB    128GB  carla    basica
------------------------------------------------
 Maquinas       : 6
 Memoria total  : 88GB   (media 14GB)
 Disco total    : 2688GB
================================================
[2026-09-15 12:04:18] [INFO ] relatorio exibido na tela (6 maquinas)

# Com filtros
$ ./bin/relatorio.sh -s suporte -m 16 2>/dev/null | grep -A3 '^lab'
lab04    suporte     32GB   1024GB  bruno    servidor
------------------------------------------------
 Maquinas       : 1
 Memoria total  : 32GB   (media 32GB)

# Gravando em arquivo e validando os erros
$ ./bin/relatorio.sh -o /tmp/inventario.txt
[2026-09-15 12:05:02] [INFO ] relatorio iniciado (setor=todos mem_min=0GB)
[2026-09-15 12:05:02] [INFO ] relatorio gravado em /tmp/inventario.txt (6 maquinas)

$ ./bin/relatorio.sh -m abc; echo "codigo=$?"
[2026-09-15 12:05:20] [ERROR] -m exige um numero inteiro, recebi 'abc'
codigo=1

$ ./bin/relatorio.sh -m 999; echo "codigo=$?"
[2026-09-15 12:05:31] [INFO ] relatorio iniciado (setor=todos mem_min=999GB)
[2026-09-15 12:05:31] [ERROR] nenhuma maquina atende aos filtros
codigo=1
Repare no : dentro do case: é o comando nulo do shell, que não faz nada e devolve 0. Ele existe porque case não aceita ramo vazio. E note "$s") como padrão — o case compara o valor da variável $setor contra o setor da linha, resolvendo o filtro sem um if aninhado.

Conferência Final

Rode os comandos abaixo e compare com a saída esperada:

$ cd ~/oficina
$ pwd
/home/aluno/oficina

$ ls -R
.:
bin  dados  fichas  logs

./bin:
busca.sh  ficha.sh  lib-log.sh  ola.sh  relatorio.sh

./dados:
maquinas.csv  setores.txt

./fichas:
lab01.txt  lab02.txt  lab03.txt  lab04.txt  lab05.txt  lab06.txt

./logs:
oficina.log

# 4 scripts executáveis — e a biblioteca NÃO executável
$ ls -l bin | grep -c '^-rwx'
4
$ [[ -x bin/lib-log.sh ]] && echo "ERRADO: biblioteca executavel" || echo "ok: biblioteca nao executavel"
ok: biblioteca nao executavel

# Todo script começa com o shebang certo
$ head -1 bin/ola.sh bin/ficha.sh bin/busca.sh bin/relatorio.sh | grep -c '^#!/usr/bin/env bash'
4

# Nenhum aviso do shellcheck
$ shellcheck bin/*.sh && echo "shellcheck limpo"
shellcheck limpo

# 6 fichas, 7 linhas cada
$ ls fichas | wc -l
6
$ wc -l fichas/*.txt | tail -1
  42 total

# O log acumulou as execuções
$ wc -l < logs/oficina.log
13             # varia conforme quantas vezes você rodou
$ grep -c ERROR logs/oficina.log
2              # os dois erros propositais do passo 43

# O relatório fecha com os números certos
$ ./bin/relatorio.sh 2>/dev/null | grep -E 'Maquinas|Memoria total|Disco total'
 Maquinas       : 6
 Memoria total  : 88GB   (media 14GB)
 Disco total    : 2688GB

Autoavaliação — marque o que você concluiu

  • Escrevi um script do zero com shebang, chmod +x e execução por ./
  • Explico por que ./ é necessário e o que o PATH tem a ver com isso
  • Provei na prática a diferença entre bash script e source script
  • Li e usei o exit code em $?, && e ||
  • Vi o word splitting quebrar um rm e sei que a cura é "$var"
  • Usei ${var:-padrão}, ${#var}, ${var##*/} e ${var/a/b}
  • Capturei saída com $( ) e fiz contas com $(( )), sabendo que não há decimais
  • Criei um array e percorri com "${arr[@]}"
  • Validei $# antes de agir e mandei a mensagem de erro para o stderr
  • Processei flags com getopts, incluindo \?, : e shift $(( OPTIND - 1 ))
  • Sei quando usar -eq e quando usar ==, e por que [[ "10" > "9" ]] é falso
  • Usei -f, -d, -s, -r, -x e ! em testes de arquivo
  • Ramifiquei com if/elif/else e com case/esac, incluindo o ramo *)
  • Percorri lista, glob, contador e arquivo com for, while e until
  • Li um CSV com while IFS=, read -r e sei por que cat | while perde as variáveis
  • Escrevi funções com local, devolvendo dado por stdout e status por return
  • Montei uma biblioteca carregada com source e a mantive sem +x
  • Provei a diferença entre >arq 2>&1 e 2>&1 >arq contando linhas
  • Usei tee, xargs e <(comando) em pipelines reais
  • Gerei texto com here-document e sei o efeito de <<'EOF'
  • Vi set -x mostrar o comando já expandido
  • Vi um script sem set -e falhar e ainda assim sair com código 0
  • Garanti limpeza com trap ... EXIT, inclusive no caminho de erro
  • Passei todos os meus scripts no shellcheck sem nenhum aviso
  • A estrutura final bate exatamente com a saída de ls -R acima

Exercícios Variados — 12 Mini-Desafios

Cada mini-desafio é independente e diz o objetivo, não o comando. Descubra a sintaxe você mesmo. Faça todos dentro de ~/treino-shell — crie a pasta com mkdir ~/treino-shell e apague tudo no final com rm -r ~/treino-shell. A "verificação" ao lado é o que prova que você acertou.

# Objetivo Dica Verificação
1 Escreva eco.sh que imprime cada argumento recebido em uma linha, numerado for sobre "$@" + um contador ./eco.sh a b c → 3 linhas
2 Faça eco.sh sair com código 2 se receber zero argumentos, imprimindo o uso no stderr [[ $# -eq 0 ]] + >&2 + exit 2 ./eco.sh >/dev/null; echo $?2
3 Escreva par.sh N que diz se N é par ou ímpar, recusando entrada não numérica $(( N % 2 )) e [[ $1 =~ ^[0-9]+$ ]] ./par.sh 7 → ímpar; ./par.sh xy → erro
4 Crie 20 arquivos log-01.txtlog-20.txt com um único comando Brace expansion com zero à esquerda ls log-*.txt | wc -l20
5 Escreva um loop que renomeia todos os log-NN.txt para log-NN.log for f in log-*.txt + mv "$f" "${f%.txt}.log" ls *.txt 2>/dev/null | wc -l0
6 Some a terceira coluna de um CSV com um while read, sem usar awk IFS=, + total=$(( total + campo )) O total tem que bater com awk -F, '{s+=$3}END{print s}'
7 Escreva a função maior a b que imprime o maior dos dois números local + (( )) + echo x=$(maior 9 14); echo $x14
8 Crie saudacao.sh que aceita -n nome e -u (maiúsculas) getopts ":n:u" + ${nome^^} ./saudacao.sh -n ana -uOLA, ANA
9 Faça um script gravar stdout e stderr no mesmo arquivo e prove com wc -l A ordem é > arq 2>&1; teste a ordem invertida também As duas contagens de linhas têm que ser diferentes
10 Gere um config.ini com here-doc em que $USER apareça literal Delimitador entre aspas simples grep -c '\$USER' config.ini1
11 Escreva um script que cria um mktemp -d e o remove mesmo se você der Ctrl+C trap ... EXIT e trap ... INT + um sleep 30 ls -d /tmp/tmp.* 2>/dev/null | wc -l0
12 Compare as listas de /bin e /usr/bin sem criar arquivo temporário diff <(ls /bin) <(ls /usr/bin) Nenhum arquivo novo em ~/treino-shell

Desafio Individual

⚡ Desafio — "Plantão da Madrugada"

Duração: 30 minutos  |  Formato: Individual, sem consultar o roteiro acima  |  Entregável: ~/desafio04/plantao.sh + ~/desafio04/prova.txt

Missão

O laboratório passou a ter plantão noturno e ninguém quer acordar às 3h para conferir máquina. Escreva um único script, plantao.sh, que lê o inventário, aplica limites recebidos por linha de comando, grava um log com carimbo de tempo e sai com código diferente de zero quando houver alerta — para que um cron possa disparar e-mail sozinho.

~/desafio04/
├── plantao.sh          (o script — executável, aprovado no shellcheck)
├── lib-plantao.sh      (funções de log — carregado com source, SEM +x)
├── inventario.csv      (8 maquinas, cabecalho igual ao de maquinas.csv)
├── saida/
│   ├── alertas.txt     (uma linha por maquina fora do limite)
│   └── resumo.txt      (gerado por here-document)
├── plantao.log         (escrito via tee pela biblioteca)
└── prova.txt           (gerado no ultimo passo)

Requisitos obrigatórios

  • Cabeçalho com #!/usr/bin/env bash e set -euo pipefail
  • Flags via getopts: -m <memoria_min>, -d <disco_min>, -s <setor>, -o <dir_saida> e -h, todas com padrão razoável
  • Tratar \? e : no case do getopts e encerrar com exit 1
  • Validar que o CSV existe e é legível, e que -m/-d receberam números — abortando com mensagem no stderr
  • Ler o CSV com while IFS=, read -r ... done < <(tail -n +2 ...) — nada de cat | while
  • Filtrar por setor com case, aceitando todos
  • Pelo menos uma função com local que devolve dado por stdout, e uma que devolve status por return
  • Contar alertas em uma variável e usar aritmética $(( )) para o total e a média
  • trap que remove o temporário do mktemp em qualquer saída
  • resumo.txt gerado por here-document com variáveis expandidas
  • Log com timestamp via tee -a, e as mensagens de WARN/ERROR indo para o stderr
  • Exit code 0 se nenhum alerta; 1 se houver alerta; 2 em erro de uso
  • shellcheck plantao.sh lib-plantao.sh sem nenhum aviso

Esqueleto inicial

#!/usr/bin/env bash
set -euo pipefail

BASE="$HOME/desafio04"
CSV="$BASE/inventario.csv"
source "$BASE/lib-plantao.sh"

mem_min=8
disco_min=200
setor="todos"
dir_saida="$BASE/saida"
alertas=0

TMP="$(mktemp)"
trap 'rm -f "$TMP"' EXIT

# 1. getopts aqui (não esqueça o shift $(( OPTIND - 1 )))

# 2. validações — CSV legível, -m e -d numéricos, dir_saida criável

# 3. funções: uma que classifica (stdout), uma que testa limite (return)

# 4. while IFS=, read -r ... done < <(tail -n +2 "$CSV")

# 5. here-document para o resumo.txt

# 6. saída final
if (( alertas > 0 )); then
    log_warn "$alertas alerta(s) encontrado(s)"
    exit 1
fi
log_info "plantao sem alertas"
exit 0

Passo final — gerar a prova

$ cd ~/desafio04
$ echo "Aluno: SEU NOME AQUI" > prova.txt
$ echo "Exercicio: 04 — Shell Scripting e Automacao" >> prova.txt
$ echo "Data: $(date '+%Y-%m-%d %H:%M')" >> prova.txt
$ echo "--- shellcheck ---" >> prova.txt
$ shellcheck plantao.sh lib-plantao.sh >> prova.txt 2>&1; echo "rc=$?" >> prova.txt
$ echo "--- estrutura ---" >> prova.txt
$ ls -lR >> prova.txt
$ echo "--- execucao sem alertas ---" >> prova.txt
$ ./plantao.sh -m 1 -d 1 >> prova.txt 2>&1; echo "rc=$?" >> prova.txt
$ echo "--- execucao com alertas ---" >> prova.txt
$ ./plantao.sh -m 64 -d 4000 >> prova.txt 2>&1; echo "rc=$?" >> prova.txt
$ echo "--- erro de uso ---" >> prova.txt
$ ./plantao.sh -m abc >> prova.txt 2>&1; echo "rc=$?" >> prova.txt
$ echo "--- resumo gerado ---" >> prova.txt
$ cat saida/resumo.txt >> prova.txt

# Confira antes de entregar — os três rc devem ser 0, 1 e 2
$ grep '^rc=' prova.txt
rc=0
rc=0
rc=1
rc=2

Bônus (opcional)

  • Adicione --dry-run: o script mostra o que faria, sem escrever nenhum arquivo
  • Faça o plantao.sh aceitar o CSV por stdin quando nenhum arquivo for passado ([[ -t 0 ]] detecta se há pipe)
  • Acrescente -v que liga set -x só na parte do processamento
  • Some memória e disco com awk em process substitution e compare com o total calculado pelo loop — os dois têm que bater
  • Agende com crontab -e para rodar a cada 5 minutos e confirme no plantao.log (lembre-se: o cron tem PATH mínimo e não roda o seu .bashrc)

Erros Comuns e Como Sair Deles

Mensagem / sintoma O que realmente aconteceu Como resolver
Permission denied ao rodar ./script.sh O arquivo não tem o bit de execução chmod +x script.sh — ou rode bash script.sh
command not found com o script na pasta atual O diretório atual não está no PATH Use ./script.sh, com o ./ na frente
bash: nome: command not found ao atribuir Você pôs espaço em volta do = nome="valor", colado dos dois lados
bad interpreter: No such file or directory Shebang errado ou o arquivo tem quebras de linha do Windows (CRLF) file script.sh para confirmar; corrija com dos2unix ou sed -i 's/\r$//'
[: too many arguments / binary operator expected Variável com espaço, sem aspas, dentro de [ ] Use [[ ]] e sempre "$var"
unbound variable set -u ativo e a variável nunca foi definida Dê um padrão: "${var:-}", ou defina antes de usar
syntax error near unexpected token 'fi' Faltou ; antes do then, ou then/do na linha errada if cond; then — o ; é obrigatório na mesma linha
syntax error: unexpected end of file Faltou fechar fi, done, esac, } ou uma aspa bash -n script.sh checa a sintaxe sem executar
O script "não vê" a variável que o loop alterou O loop rodou em subshell por causa do cat arquivo | Troque por done < arquivo ou done < <(comando)
Só metade da saída foi para o arquivo de log 2>&1 veio antes do > comando > log.txt 2>&1 — o 2>&1 por último
O cd do script não mudou o seu terminal O script rodou em processo filho É o comportamento certo; se quiser afetar o shell atual, use source
$(( 10 / 4 )) devolveu 2 Bash só faz aritmética inteira awk 'BEGIN{print 10/4}' ou bc -l
Script morre em (( contador++ )) sob set -e A expressão avaliou 0, o que vira exit code 1 contador=$(( contador + 1 )), ou (( contador++ )) || true
Funciona no terminal e falha no cron O cron tem PATH mínimo e não carrega o .bashrc Use caminhos absolutos e defina o PATH no topo do script
O loop tratou "nota de aula.txt" como 3 arquivos Word splitting — faltaram aspas ou você iterou sobre $(ls) for f in *.txt e sempre "$f"

Referência Rápida de Shell Scripting

Cabeçalho padrão

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

trap 'rm -f "$TMP"' EXIT

Quatro linhas que transformam um rascunho em script de produção.

Variáveis especiais

  • $0 — nome do script
  • $1$9 — argumentos posicionais
  • $# — quantidade de argumentos
  • "$@" — todos, como itens separados
  • $? — exit code do último comando
  • $$ — PID do script
  • $! — PID do último processo em background

Expansões

  • ${v:-padrao} — padrão se vazia
  • ${v:=padrao} — padrão e grava
  • ${v:?msg} — aborta se vazia
  • ${#v} — comprimento
  • ${v##*/} — basename
  • ${v%/*} — dirname
  • ${v/a/b} · ${v//a/b} — troca
  • ${v^^} · ${v,,} — caixa

Testes

  • -f arquivo · -d diretório · -e existe
  • -s não vazio · -r -w -x permissões
  • -z string vazia · -n não vazia
  • -eq -ne -gt -ge -lt -le — números
  • == != < > — strings
  • =~ — regex (só em [[ ]])
  • ! nega · && E · || OU

Redirecionamento

  • > sobrescreve · >> acrescenta
  • 2> só stderr · < entrada
  • > f 2>&1 — tudo no arquivo ✓
  • 2>&1 > f — pega metade ✗
  • &> f — atalho do Bash
  • >/dev/null 2>&1 — silêncio total
  • | tee -a f — grava e deixa passar
  • <(cmd) — saída vira arquivo

Estruturas

  • if cond; then … fi
  • case "$v" in p) … ;; *) … ;; esac
  • for x in lista; do … done
  • for (( i=0; i<n; i++ ))
  • while cond; do … done
  • until cond; do … done
  • while IFS= read -r l; do … done < f
  • f() { local x="$1"; echo "$x"; }

Checklist de Avaliação

Critério Iniciante Intermediário Avançado
Estrutura do script Escreve comandos em sequência Usa shebang, chmod +x e comentário de uso Adota set -euo pipefail e trap por padrão, e explica cada opção
Quoting Esquece as aspas e não vê problema Usa "$var" depois de ser corrigido Prevê o word splitting antes de rodar e passa no shellcheck de primeira
Argumentos Usa $1 sem validar Valida $# e imprime o uso no stderr Implementa getopts completo, com \?, : e OPTIND
Condicionais Escreve if simples Escolhe entre -eq e == corretamente Usa case, =~ e testes de arquivo combinados
Loops Faz for sobre lista fixa Percorre glob e lê arquivo com while read -r Explica por que cat | while perde variáveis e usa < <( )
Funções Copia trechos repetidos Cria funções com local Separa dado (stdout) de status (return) e mantém biblioteca com source
Redirecionamento Usa > e >> Separa stdout de stderr e usa tee Explica a ordem de 2>&1 e usa process substitution
Depuração Espalha echo pelo script Usa bash -x e lê a mensagem de erro inteira Roda shellcheck por reflexo e prevê o erro antes de executar
Mini-desafios Faz até 4 com ajuda Faz de 5 a 9 sozinho Faz os 12 em menos de 20 minutos

Pontuação Sugerida

Iniciante — 0 a 5 pts

Completa os 43 passos seguindo o roteiro, mas esquece aspas, não valida argumentos e não conclui o desafio sem ajuda.

Intermediário — 6 a 8 pts

Os cinco scripts funcionam, o relatorio.sh passa no shellcheck, o desafio roda e devolve os exit codes certos na maioria dos casos.

Avançado — 9 a 10 pts

Conclui desafio e bônus, entrega os três exit codes (0, 1, 2) corretos, zero avisos no shellcheck e explica cada linha do cabeçalho defensivo.

Entrega

O que enviar

  • O conteúdo de ~/desafio04/prova.txt (copie e cole no formulário da disciplina)
  • A saída de ls -lR ~/oficina mostrando a estrutura final do roteiro
  • O conteúdo de ~/oficina/bin/relatorio.sh
  • Quantos dos 12 mini-desafios você concluiu sozinho
  • Em 3 linhas: explique com suas palavras o que aconteceu no passo 40 — por que um script que falhou saiu com código 0, e o que set -e muda nisso
Comando único que gera tudo o que deve ser entregue
$ echo "=== ENTREGA — Exercicio 04 — SEU NOME ===" > ~/entrega04.txt
$ echo "Data: $(date '+%F %H:%M')" >> ~/entrega04.txt
$ echo "--- oficina ---" >> ~/entrega04.txt
$ ls -lR ~/oficina >> ~/entrega04.txt
$ echo "--- shellcheck da oficina ---" >> ~/entrega04.txt
$ shellcheck ~/oficina/bin/*.sh >> ~/entrega04.txt 2>&1; echo "rc=$?" >> ~/entrega04.txt
$ echo "--- relatorio.sh ---" >> ~/entrega04.txt
$ cat ~/oficina/bin/relatorio.sh >> ~/entrega04.txt
$ echo "--- saida do relatorio ---" >> ~/entrega04.txt
$ ~/oficina/bin/relatorio.sh >> ~/entrega04.txt 2>&1
$ echo "--- desafio04 ---" >> ~/entrega04.txt
$ cat ~/desafio04/prova.txt >> ~/entrega04.txt
$ wc -l ~/entrega04.txt

Recursos e Referências

Documentação no próprio sistema

  • 📖 man bash — a referência completa (longa, mas definitiva)
  • 📖 help test — todos os operadores de teste
  • 📖 help getopts · help trap · help set
  • 📖 bash -n script.sh — checa sintaxe sem executar
  • 📖 bash --version — descubra sua versão

Orientações Gerais

Como aproveitar melhor este exercício

  • Antes de rodar, diga qual será o exit code. Depois confira com echo $? — é o exercício mental mais valioso desta página
  • Rode bash -n script.sh sempre que salvar: ele acha erro de sintaxe sem executar nada
  • Quando um script se comportar de forma estranha, ligue bash -x antes de sair colocando echo
  • Passe todo script pelo shellcheck antes de considerá-lo pronto — o objetivo é saída vazia
  • Escreva o set -euo pipefail junto com o shebang, por reflexo, mesmo em script de dez linhas
  • Teste sempre o caminho de erro: chame o script sem argumentos, com argumento inválido e com arquivo inexistente
  • Refaça o roteiro do zero (rm -r ~/oficina) até conseguir escrever o relatorio.sh sem consultar

Erros conceituais a evitar

  • Achar que aspas são "estilo" — elas mudam quantos argumentos o comando recebe
  • Confundir -eq (número) com == (texto) e concluir que "10" é menor que "9"
  • Esperar que return 5 devolva o número 5 para x=$(funcao)
  • Escrever for f in $(ls) em vez de for f in *
  • Usar cat arquivo | while read e depois estranhar que o contador zerou
  • Escrever 2>&1 > log achando que junta os dois streams
  • Esquecer o shift $(( OPTIND - 1 )) e receber as flags como se fossem argumentos
  • Confiar em um script que terminou "sem mensagem" sem nunca ter olhado o exit code
  • Dar chmod +x em uma biblioteca que deveria ser carregada com source
  • Supor que o script roda no diretório onde ele mora — ele roda no diretório de quem o chamou