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 +xe execução por caminho - Explicar a diferença entre
./script.sh,bash script.shesource 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,$#,"$@",shiftegetopts - Escrever testes de string, número, arquivo e diretório e ramificar com
ifecase - Percorrer listas, arquivos e contadores com
for,whileeuntil - 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
stdoutestderrna ordem correta e usartee, pipes e here-documents - Garantir limpeza com
trap ... EXITe depurar comset -xeshellcheck - 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 $SHELLebash --version - Um editor de texto:
nanoserve (aqui ele é permitido) shellcheckno passo 42 — se não tiver, use shellcheck.net- Pré-requisitos: Módulo 02 e os Exercícios 01–03
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 oecho $? - 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
Cronograma do Exercício
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)
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
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
#!/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)"
#!). 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/bin — o 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
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
$ 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
./; 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
$ 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.
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}]" []
${ } 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/
$ 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
"$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
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
$ 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
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
>&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
#!/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
{ ... } > 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
":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 (( ))
[[ "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
[[ ]] 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
#!/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
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.
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
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
$ 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
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.
$ 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
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.
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)
$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
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
# 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
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ídas — diff, 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
<<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.
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
+ é 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
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
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
EXIT dispara em toda forma de término: fim normal,
exit explícito, erro sob set -e e — se você também tratar
INT — Ctrl+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
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
#!/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
: 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 +xe execução por./ - Explico por que
./é necessário e o que oPATHtem a ver com isso - Provei na prática a diferença entre
bash scriptesource script - Li e usei o exit code em
$?,&&e|| - Vi o word splitting quebrar um
rme 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 ostderr - Processei flags com
getopts, incluindo\?,:eshift $(( OPTIND - 1 )) - Sei quando usar
-eqe quando usar==, e por que[[ "10" > "9" ]]é falso - Usei
-f,-d,-s,-r,-xe!em testes de arquivo - Ramifiquei com
if/elif/elsee comcase/esac, incluindo o ramo*) - Percorri lista, glob, contador e arquivo com
for,whileeuntil - Li um CSV com
while IFS=, read -re sei por quecat | whileperde as variáveis - Escrevi funções com
local, devolvendo dado por stdout e status porreturn - Montei uma biblioteca carregada com
sourcee a mantive sem+x - Provei a diferença entre
>arq 2>&1e2>&1 >arqcontando linhas - Usei
tee,xargse<(comando)em pipelines reais - Gerei texto com here-document e sei o efeito de
<<'EOF' - Vi
set -xmostrar o comando já expandido - Vi um script sem
set -efalhar e ainda assim sair com código 0 - Garanti limpeza com
trap ... EXIT, inclusive no caminho de erro - Passei todos os meus scripts no
shellchecksem nenhum aviso - A estrutura final bate exatamente com a saída de
ls -Racima
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.
Desafio Individual
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 basheset -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:nocasedogetoptse encerrar comexit 1 - Validar que o CSV existe e é legível, e que
-m/-dreceberam números — abortando com mensagem no stderr - Ler o CSV com
while IFS=, read -r ... done < <(tail -n +2 ...)— nada decat | while - Filtrar por setor com
case, aceitandotodos - Pelo menos uma função com
localque devolve dado por stdout, e uma que devolve status porreturn - Contar alertas em uma variável e usar aritmética
$(( ))para o total e a média trapque remove o temporário domktempem qualquer saídaresumo.txtgerado por here-document com variáveis expandidas- Log com timestamp via
tee -a, e as mensagens deWARN/ERRORindo para o stderr - Exit code
0se nenhum alerta;1se houver alerta;2em erro de uso shellcheck plantao.sh lib-plantao.shsem 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.shaceitar o CSV porstdinquando nenhum arquivo for passado ([[ -t 0 ]]detecta se há pipe) - Acrescente
-vque ligaset -xsó na parte do processamento - Some memória e disco com
awkem process substitution e compare com o total calculado pelo loop — os dois têm que bater - Agende com
crontab -epara rodar a cada 5 minutos e confirme noplantao.log(lembre-se: ocrontemPATHmínimo e não roda o seu.bashrc)
Erros Comuns e Como Sair Deles
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
-farquivo ·-ddiretório ·-eexiste-snão vazio ·-r -w -xpermissões-zstring vazia ·-nnão vazia-eq -ne -gt -ge -lt -le— números== != < >— strings=~— regex (só em[[ ]])!nega ·&&E ·||OU
Redirecionamento
>sobrescreve ·>>acrescenta2>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 … ficase "$v" in p) … ;; *) … ;; esacfor x in lista; do … donefor (( i=0; i<n; i++ ))while cond; do … doneuntil cond; do … donewhile IFS= read -r l; do … done < ff() { local x="$1"; echo "$x"; }
Checklist de Avaliação
Pontuação Sugerida
Completa os 43 passos seguindo o roteiro, mas esquece aspas, não valida argumentos e não conclui o desafio sem ajuda.
Os cinco scripts funcionam, o relatorio.sh passa no shellcheck,
o desafio roda e devolve os exit codes certos na maioria dos casos.
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 ~/oficinamostrando 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 -emuda nisso
$ 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
LPI (referência principal)
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
Leitura complementar
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.shsempre que salvar: ele acha erro de sintaxe sem executar nada - Quando um script se comportar de forma estranha, ligue
bash -xantes de sair colocandoecho - Passe todo script pelo
shellcheckantes de considerá-lo pronto — o objetivo é saída vazia - Escreva o
set -euo pipefailjunto 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 orelatorio.shsem 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 5devolva o número 5 parax=$(funcao) - Escrever
for f in $(ls)em vez defor f in * - Usar
cat arquivo | while reade depois estranhar que o contador zerou - Escrever
2>&1 > logachando 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 +xem uma biblioteca que deveria ser carregada comsource - Supor que o script roda no diretório onde ele mora — ele roda no diretório de quem o chamou