Seu código roda em Linux — a produção, o container, o CI, a nuvem

Apostila completa de Linux & Shell Scripting para Programadores

Você não precisa ser sysadmin, mas todo programador vive no shell: depurar um container, ler um log em produção, escrever o script de deploy, entender por que o CI falhou. Esta apostila cobre o Linux que um dev usa de verdade — sistema de arquivos e permissões, o toolkit de texto (grep/sed/awk/jq), processos e sinais, e sobretudo o shell como linguagem: quoting, scripts robustos com set -euo pipefail, e as ferramentas do dia a dia (ssh, rsync, tmux, systemd).

10 módulosPermissões · pipes · grep/sed/awk/jqSinais · quoting · set -euo pipefailssh · rsync · tmux · systemdBoxes de entrevistaExercícios com gabarito
MÓDULO 01 · BÁSICO

Por que Linux e a filosofia Unix

Objetivo: entender por que o shell é uma competência de todo programador, a filosofia Unix que explica como as ferramentas se encaixam, e o vocabulário (shell, terminal, bash).

1.1 Onde você encontra Linux (mesmo se seu laptop é Windows/Mac)

1.2 A filosofia Unix

💡 As três ideias que explicam tudo
  1. Programas pequenos que fazem uma coisa bem. grep só busca. sort só ordena. wc só conta.
  2. Texto é a interface universal. A saída de um programa é texto que serve de entrada para o próximo. Não há "objetos" nem APIs — há linhas.
  3. Componha com pipes. | liga a saída de um comando à entrada do próximo. Ferramentas simples viram soluções complexas por composição.

Exemplo: "quais os 5 IPs que mais aparecem no log de acesso?" → cut -d' ' -f1 access.log | sort | uniq -c | sort -rn | head -5. Cinco ferramentas triviais, uma resposta útil, sem escrever um programa.

1.3 Shell × terminal × bash

TermoO que é
Terminal (emulador)O programa gráfico que mostra o texto e recebe suas teclas (GNOME Terminal, iTerm2, Windows Terminal, Alacritty, Kitty)
ShellO interpretador que lê seus comandos e os executa — o "programa" que roda dentro do terminal
bashO shell mais comum em Linux (Bourne Again SHell). zsh é o padrão no macOS moderno e muito popular; sh geralmente aponta para um shell POSIX mínimo (dash em Debian/Ubuntu); fish é amigável mas não compatível com POSIX
POSIX shO subconjunto padronizado; scripts que precisam rodar em qualquer lugar (inclusive Alpine, que não tem bash por padrão) devem ser POSIX-sh
⚠️ #!/bin/bash vs #!/bin/sh

Um script com #!/bin/sh deve usar só sintaxe POSIX — [[ ]], arrays, {1..10}, local podem não funcionar. Se você usa recursos de bash, declare #!/usr/bin/env bash e garanta que bash existe (não existe por padrão numa imagem Alpine). Muitos bugs de CI são "funcionou no meu Ubuntu, quebrou no Alpine do container".

💼 Mercado de trabalho

Shell é competência esperada — não é vaga de "shell scripter", é parte de toda vaga de backend, DevOps, SRE, dados, plataforma. Perguntas de abertura: "Explique a filosofia Unix" (ferramentas pequenas + texto + pipes), "Diferença entre shell e terminal", "bash vs sh — quando importa?" (portabilidade; Alpine não tem bash), "Como você resolveria 'top 10 endpoints mais lentos no log' na linha de comando?".

✏️ Exercício 1 — Componha

Um arquivo vendas.csv tem colunas data,produto,valor separadas por vírgula. Escreva um pipeline que mostre os 3 produtos com maior soma de valor. (Descreva o raciocínio; a resposta exata varia.)

Gabarito (uma solução): tail -n +2 vendas.csv | awk -F, '{soma[$2] += $3} END {for (p in soma) print soma[p], p}' | sort -rn | head -3. Raciocínio: tail -n +2 pula o cabeçalho; awk -F, separa por vírgula, acumula $3 (valor) num array indexado por $2 (produto), e no END imprime "soma produto"; sort -rn ordena numericamente decrescente; head -3 pega os três primeiros. Uma ferramenta faz o parsing+agregação, outra ordena, outra corta — filosofia Unix.

MÓDULO 02 · BÁSICO

Sistema de arquivos, navegação e permissões

Objetivo: a árvore de diretórios do Linux, navegar e manipular arquivos com confiança, globbing e find, e o modelo de permissões.

2.1 A árvore (FHS — Filesystem Hierarchy Standard)

DiretórioO que guarda
/A raiz; tudo pendura daqui (não há "drive C:")
/etcArquivos de configuração do sistema (texto)
/varDados variáveis: /var/log (logs), /var/lib (estado de serviços), /var/cache
/usrProgramas e bibliotecas do sistema (/usr/bin, /usr/lib)
/home/<user>Diretório pessoal (~); seus arquivos e config de usuário
/tmpTemporários; limpo no boot (ou por política). /dev/shm é tmpfs (RAM)
/optSoftware de terceiros "auto-contido"
/proc, /sysSistemas de arquivos virtuais: estado do kernel e dos processos (/proc/<pid>)
/devArquivos de dispositivo (/dev/null, /dev/sda)

2.2 Navegar e manipular

pwd                    # onde estou
cd /var/log ; cd - ; cd ~   # ir; voltar ao anterior; ir para home
ls -lah                # long, all (ocultos), human-readable
cp -r src/ dst/        # -r para diretórios; -i pergunta antes de sobrescrever
mv a b ; mkdir -p x/y/z ; rm -rf lixo/   # -p cria a cadeia; -rf recursivo forçado (CUIDADO)
ln -s /caminho/real link   # symlink (atalho); sem -s é hard link
⚠️ rm -rf não tem lixeira

rm apaga definitivamente — não há "Ctrl+Z", não há Trash. rm -rf $DIR/ com $DIR vazio vira rm -rf /. Hábitos que salvam: rode ls no glob antes de rm; use rm -i em interativo; em scripts, valide a variável ([[ -n "$DIR" ]]) e prefira find ... -delete com condições explícitas; considere trash-cli no dia a dia.

2.3 Globbing (o shell expande, não o comando)

2.4 find — o canivete

find . -name "*.log" -type f -mtime +7 -size +10M        # logs .log, arquivos, >7 dias, >10MB
find /var/log -name "*.gz" -mtime +30 -delete            # apagar antigos
find . -type f -name "*.tmp" -exec rm {} +               # executar um comando por lote de resultados
find . -type d -empty                                     # diretórios vazios

-exec ... {} + agrupa os resultados numa chamada só (eficiente); {} \; chama uma vez por arquivo.

2.5 Permissões

-rwxr-xr--  1 ana  devs  4096  ago 30 14:00  deploy.sh
│└┬┘└┬┘└┬┘    │    │
│ │  │  └── outros (o): r--   (só leitura)
│ │  └───── grupo   (g): r-x   (ler e executar)
│ └──────── dono    (u): rwx   (ler, escrever, executar)
└────────── tipo: - arquivo | d diretório | l link
BitEm arquivoEm diretório
r (4)ler o conteúdolistar os nomes (ls)
w (2)modificar o conteúdocriar/apagar/renomear entradas dentro dele
x (1)executar"entrar" (cd) e acessar arquivos por caminho
chmod 750 deploy.sh          # u=rwx(7) g=rx(5) o=nada(0)  → octal
chmod u+x,go-w deploy.sh     # simbólico: adiciona x ao dono, remove w de grupo/outros
chown ana:devs deploy.sh     # dono:grupo (precisa de root/sudo)
umask 022                    # máscara de criação: novos arquivos nascem 644, dirs 755
💼 Mercado de trabalho

Perguntas: "O que significa chmod 755?" (rwx para dono, r-x para grupo e outros), "O que o bit x faz num diretório?" (permite cd e acesso por caminho), "Diferença entre symlink e hard link" (symlink aponta para um caminho; hard link é outro nome para o mesmo inode), "Por que find ... -exec {} + é melhor que {} \;?" (uma invocação por lote vs por arquivo), "Como apagar arquivos com mais de 30 dias?".

✏️ Exercício 2 — Permissões e find

(a) Um script backup.sh precisa ser executável pelo dono e pelo grupo, e nem legível pelos outros. Qual chmod? (b) Escreva um find que remova arquivos *.log em /var/log/app com mais de 14 dias, mas não desça em subdiretórios.

Gabarito: (a) chmod 770 backup.sh — u=rwx(7), g=rwx(7), o=--- (0). Se o grupo não precisa escrever: 750. (b) find /var/log/app -maxdepth 1 -type f -name "*.log" -mtime +14 -delete-maxdepth 1 impede a recursão; -type f evita pegar diretórios; -mtime +14 = modificado há mais de 14 dias; -delete remove. Antes de rodar com -delete, troque por -print para conferir a lista.

MÓDULO 03 · INTERMEDIÁRIO

Manipular texto: pipes, redirecionamento, grep, sed, awk, jq

Objetivo: os três descritores padrão e o redirecionamento, e o toolkit de processamento de texto que resolve 90% das tarefas de linha de comando.

3.1 stdin, stdout, stderr e redirecionamento

DescritorRedireciona com
stdin (entrada)0< arquivo
stdout (saída normal)1> arquivo (sobrescreve), >> arquivo (anexa)
stderr (erros)22> arquivo; 2>&1 junta stderr no stdout; &> arquivo junta os dois
comando > out.txt 2> err.txt        # separa saída e erros
comando > tudo.txt 2>&1             # junta os dois no mesmo arquivo (ordem importa!)
comando 2>&1 | grep erro            # manda stderr para o pipe também
comando < entrada.txt               # stdin de um arquivo
comando > /dev/null 2>&1            # descarta tudo

cat <<'EOF' > config.txt            # here-doc: bloco de texto literal (aspas em EOF = sem expansão)
porta=8080
host=localhost
EOF

grep foo <<< "$VARIAVEL"            # here-string: passa uma string como stdin
💡 O gargalo do > arquivo 2>&1

A ordem lê-se da esquerda para a direita: > arquivo primeiro aponta stdout para o arquivo; 2>&1 depois aponta stderr para "onde stdout está agora" (o arquivo). Se você inverter (2>&1 > arquivo), stderr vai para o terminal (onde stdout estava antes) e só stdout vai para o arquivo. Erro clássico.

3.2 O toolkit de leitura

3.3 grep — buscar

grep -i "error" app.log            # -i case-insensitive
grep -rn "TODO" src/               # -r recursivo, -n com número de linha
grep -v "healthcheck" access.log   # -v inverte (linhas que NÃO casam)
grep -E "warn|error|fatal" app.log # -E regex estendida (ou grep -e ... -e ...)
grep -c "200 " access.log          # -c conta as linhas que casam
grep -A2 -B1 "Exception" app.log   # contexto: 2 linhas depois, 1 antes
grep -o "user=[a-z]*" app.log      # -o só a parte que casou

3.4 sed — editar fluxo

sed 's/antigo/novo/' arq          # substitui a 1ª ocorrência por linha
sed 's/antigo/novo/g' arq         # g = global (todas na linha)
sed -i.bak 's|/old/path|/new/path|g' config   # -i edita o arquivo (com backup .bak); | como delimitador
sed -n '10,20p' arq               # -n silencia; imprime só as linhas 10 a 20
sed '/^#/d; /^$/d' config          # apaga comentários e linhas vazias

3.5 awk — colunas, filtros, agregação

awk '{print $1, $4}' access.log            # $1, $4 = campos (por espaço); $0 = linha inteira; NF = nº de campos
awk -F: '{print $1}' /etc/passwd           # -F: separador ':'
awk '$9 == 500 {print $7}' access.log      # só linhas onde o campo 9 é 500, imprime o 7
awk '$NF > 1.0 {c++} END {print c}' timings # conta linhas cujo último campo > 1.0
awk -F, '{s[$2]+=$3} END{for(k in s) print k, s[k]}' vendas.csv  # soma por chave
awk 'NR%100==0' huge.log                   # NR = nº da linha; amostra 1 a cada 100

3.6 O resto do arsenal

ComandoFaz
cut -d',' -f2,4Extrai colunas por delimitador (ou -c por posição)
sort / sort -n / sort -rn / sort -u / sort -k2Ordena (numérico, reverso, único, por coluna)
uniq -c / uniq -dColapsa duplicatas adjacentes (precisa de sort antes); -c conta
tr 'a-z' 'A-Z' / tr -d ' ' / tr -s ' 'Traduz/apaga/comprime caracteres
xargsTransforma stdin em argumentos: find . -name '*.js' | xargs wc -l. Use -0 com find -print0 para nomes com espaço; -P4 paraleliza
tee arquivoEscreve na tela E num arquivo ao mesmo tempo
paste, join, commCombinar arquivos lado a lado / por chave / comparar conjuntos ordenados

3.7 jq — JSON não é texto de linhas

curl -s api/users | jq '.[] | {id, nome: .name}'      # seleciona e reformata
jq -r '.items[].sku' data.json                        # -r = raw (sem aspas), um por linha
kubectl get pods -o json | jq '.items[] | select(.status.phase != "Running") | .metadata.name'
jq 'map(.valor) | add' vendas.json                    # soma

Regra: para texto tabular/linhas, use grep/sed/awk; para JSON, use jq (e yq para YAML). Não tente parsear JSON com regex.

💼 Mercado de trabalho

Perguntas muito comuns (às vezes ao vivo, num terminal): "Conte quantas requisições retornaram 500 neste log" (grep ' 500 ' | wc -l ou awk '$9==500'), "Top 10 URLs mais acessadas" (awk '{print $7}' | sort | uniq -c | sort -rn | head), "Substitua todas as ocorrências de X por Y em vários arquivos" (sed -i + find/grep -rl), "Extraia o campo name de um JSON" (jq -r '.name'), "Diferença entre > e >>", "O que 2>&1 faz?".

✏️ Exercício 3 — Analise o log

Um access.log no formato comum (IP no campo 1, método+URL no campo 6-7 entre aspas, status no campo 9). Escreva: (a) o total de erros 5xx; (b) as 5 URLs que mais deram 404; (c) quantos IPs únicos acessaram hoje (assumindo que o log é só de hoje).

Gabarito: (a) awk '$9 ~ /^5/' access.log | wc -l (ou grep -cE ' 5[0-9]{2} '). (b) awk '$9 == 404 {print $7}' access.log | sort | uniq -c | sort -rn | head -5. (c) awk '{print $1}' access.log | sort -u | wc -l (ou sort -u -k1,1 / awk '!seen[$1]++' | wc -l sem precisar ordenar).

MÓDULO 04 · INTERMEDIÁRIO

Processos, sinais e job control

Objetivo: ver o que está rodando, enviar sinais corretamente, controlar jobs em foreground/background, e entender exit codes e os operadores && / ||.

4.1 Ver os processos

ps aux                       # todos os processos, formato BSD (USER PID %CPU %MEM ... COMMAND)
ps -ef                       # estilo System V (com PPID)
ps aux | grep '[n]ginx'      # o [n] evita que o grep encontre a si mesmo
pgrep -af node               # PIDs (e linha de comando) que casam
top      /    htop           # interativo; htop é bem melhor (barras, filtro, kill visual)
pstree -p                    # árvore de processos

4.2 Sinais

SinalO que faz
SIGTERM15"Termine, por favor" — o processo pode fazer cleanup. O padrão do kill e o que orquestradores mandam primeiro
SIGKILL9"Morra agora" — não pode ser capturado nem ignorado; sem cleanup. Último recurso
SIGINT2Ctrl+C — interrupção pelo usuário
SIGHUP1Terminal fechou; muitos daemons usam para "recarregar config"
SIGSTOP / SIGCONT19 / 18Pausar / retomar (Ctrl+Z manda SIGTSTP)
kill 1234            # SIGTERM ao PID 1234
kill -9 1234         # SIGKILL (só se o TERM não funcionou)
kill -HUP $(pgrep nginx)   # recarregar nginx
pkill -f "python worker.py"   # por padrão de linha de comando
killall node        # por nome exato do executável
💡 Sempre TERM antes de KILL

kill -9 não deixa o processo fechar arquivos, liberar locks, terminar transações, salvar estado — pode corromper dados. O caminho certo: kill (TERM), esperar alguns segundos, e só então kill -9 se ainda estiver vivo. É exatamente o que o Kubernetes faz (SIGTERM → terminationGracePeriodSeconds → SIGKILL). Seu app deve tratar SIGTERM para graceful shutdown.

4.3 Job control

./demorado.sh &          # roda em background; imprime o PID
jobs                     # lista os jobs desta sessão
fg %1                    # traz o job 1 para foreground
Ctrl+Z                   # suspende o job em foreground
bg %1                    # retoma o job 1 em background

nohup ./servidor.sh > log.txt 2>&1 &   # imune a SIGHUP; sobrevive ao fechar o terminal
disown -h %1             # tira o job da tabela de jobs (também sobrevive ao logout)

Para tarefas que precisam mesmo sobreviver e você poder voltar a elas: tmux (Módulo 8) > nohup.

4.4 Exit codes e encadeamento

💼 Mercado de trabalho

Perguntas: "Diferença entre SIGTERM e SIGKILL" (TERM permite cleanup, KILL não pode ser capturado), "Como você reinicia um processo travado sem corromper dados?" (TERM, esperar, KILL só se preciso), "O que && e || fazem?", "Como manter um comando rodando após fechar o SSH?" (tmux; ou nohup+disown), "O que é $??", "O que 2>&1 antes ou depois do > muda?".

✏️ Exercício 4 — Cenário de produção

Um processo worker.py (você só sabe que roda em Python) está consumindo 100% de CPU há uma hora e o serviço está degradado. Descreva os comandos que você usa para diagnosticar e resolver com o mínimo de dano.

Gabarito: (1) ps aux --sort=-%cpu | head ou htop para confirmar qual PID e desde quando (ps -o pid,etime,%cpu,cmd -p <PID>). (2) pgrep -af worker.py para achar o PID exato e a linha de comando completa. (3) Antes de matar: olhar os logs do worker e, se disponível, um profile (py-spy dump --pid <PID>) para ver onde está preso — isso vira o postmortem. (4) Resolver: kill <PID> (SIGTERM) e observar se o supervisor (systemd/orquestrador) o reinicia limpo; esperar ~10–30s; só se não morrer, kill -9 <PID>. (5) Se há vários workers, matar um de cada vez para não derrubar a capacidade toda. (6) Confirmar recuperação (htop, métricas) e abrir a investigação da causa (loop, regex catastrófico, dado ruim).

MÓDULO 05 · INTERMEDIÁRIO

O shell como linguagem

Objetivo: o que todo programador tropeça — quoting — mais variáveis, substituição de comando, expansão de parâmetros, arrays e condicionais.

5.1 Quoting — o tópico mais importante do shell

FormaO que acontece dentro
Sem aspas $VARExpande a variável E faz word splitting (quebra em espaços/tabs/newlines) E globbing. Fonte nº 1 de bugs
Aspas duplas "$VAR"Expande variáveis e $(...), mas não faz splitting nem globbing. O padrão que você deve usar quase sempre
Aspas simples 'texto $VAR'Literal absoluto — nada é interpretado, nem $, nem \
# o bug clássico:
arquivo="meu relatório.pdf"
rm $arquivo        # vira: rm meu relatório.pdf  → tenta apagar "meu" e "relatório.pdf"
rm "$arquivo"      # CERTO: rm "meu relatório.pdf"

for f in *.txt; do            # glob no for: OK sem aspas (é a exceção)
  mv "$f" "processados/$f"    # mas SEMPRE aspas ao USAR a variável
done
⚠️ Regra prática: aspas duplas em toda expansão

"$var", "$(comando)", "${array[@]}", "$@". As únicas exceções deliberadas são casos onde você quer o splitting (raro e consciente). Rode ShellCheck (Módulo 6) — ele grita em cada expansão sem aspas. 90% dos bugs de script são quoting.

5.2 Variáveis e substituição de comando

nome="Ana"            # SEM espaços em volta do = (nome = "Ana" seria um comando "nome")
echo "Olá, ${nome}!"   # ${} delimita: útil em "${nome}_backup"

hoje=$(date +%F)      # command substitution: captura a saída (moderno; use isto)
count=`ls | wc -l`    # backticks: forma antiga, não aninha bem — evite
export API_URL="https://..."   # export: torna disponível para processos-filho

5.3 Expansão de parâmetros (parameter expansion)

${VAR:-padrao}     # usa "padrao" se VAR não está setada ou vazia
${VAR:=padrao}     # idem, e ATRIBUI a VAR
${VAR:?erro msg}   # aborta com a mensagem se VAR está vazia (ótimo para validar args)
${#VAR}            # comprimento
${VAR#prefixo}     # remove o menor prefixo que casa; ## = o maior
${VAR%.txt}        # remove sufixo (% menor, %% maior) — ${arquivo%.*} tira a extensão
${VAR/antigo/novo} # substitui a 1ª; // substitui todas
${VAR:0:3}         # substring (posição 0, tamanho 3)
${ARQ##*/}         # basename "manual"; ${ARQ%/*} = dirname

5.4 Outras expansões

echo {1..5}              # 1 2 3 4 5   (brace expansion)
echo arq_{a,b,c}.txt     # arq_a.txt arq_b.txt arq_c.txt
mkdir -p projeto/{src,test,docs}
echo $(( 3 * (2 + 4) ))  # aritmética: 18
i=0; (( i++ )); echo $i  # 1
echo ~ $HOME             # tilde expansion → /home/ana

5.5 Arrays e condicionais

servidores=(web1 web2 web3)
echo "${servidores[1]}"          # web2 (índice 0-based)
echo "${#servidores[@]}"        # 3 (tamanho)
for s in "${servidores[@]}"; do echo "deploy em $s"; done

# condicionais: use [[ ]] no bash (mais seguro que [ ])
if [[ -f "$arquivo" && -r "$arquivo" ]]; then echo "existe e é legível"; fi
if [[ "$env" == prod ]]; then ...; elif [[ "$env" == staging ]]; then ...; fi
if [[ "$n" -gt 10 ]]; then ...; fi       # -eq -ne -lt -le -gt -ge para números
[[ "$s" =~ ^[0-9]+$ ]] && echo "é número"      # =~ regex
TesteVerdadeiro se…
-e path / -f / -d / -Lexiste / é arquivo comum / é diretório / é symlink
-r / -w / -xlegível / gravável / executável
-s pathexiste e não está vazio
-z str / -n strstring vazia / string não vazia
str1 == str2 / !=comparação de string (em [[ ]], o lado direito pode ter glob)
💼 Mercado de trabalho

Perguntas quase certas: "Diferença entre aspas simples e duplas", "Por que rm $arquivo é perigoso e rm "$arquivo" não?" (word splitting e globbing), "Como dar um valor default a uma variável?" (${VAR:-default}), "Diferença entre [ ] e [[ ]]" ([[ ]] é builtin do bash, não faz splitting, suporta &&/=~/<), "Command substitution: backticks ou $()?" ($() — aninha e lê melhor).

✏️ Exercício 5 — Preveja a saída

Dado x="a b c", o que imprime cada linha? (a) echo $x (b) echo "$x" (c) set -- $x; echo $# (d) echo "${x// /-}" (e) y=${z:-default}; echo $y (com z não definida).

Gabarito: (a) a b c — mas passado como 3 palavras separadas (o echo as junta com espaço na exibição; a diferença aparece se fosse outro comando). (b) a b c — como uma única palavra. (c) 3set -- sem aspas fez word splitting de $x em 3 argumentos posicionais; $# é a contagem. (d) a-b-c — substitui todos os espaços por -. (e) defaultz não existe, então ${z:-default} devolve default (sem atribuir a z).

MÓDULO 06 · AVANÇADO

Escrever scripts robustos

Objetivo: o boilerplate que separa um script frágil de um confiável — set -euo pipefail, trap de limpeza, funções, argumentos, validação, e ShellCheck.

6.1 O cabeçalho que todo script deve ter

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

# set -e        : aborta o script se um comando falhar (exit != 0)
# set -u        : erro se usar variável não definida (pega typos)
# set -o pipefail: o pipe falha se QUALQUER comando dele falhar (não só o último)
# IFS restrito  : word splitting só em newline e tab, não em espaço → menos surpresa
⚠️ set -e tem armadilhas

set -e não aborta em: comandos dentro de if/&&/||, funções chamadas em condições, e alguns casos de subshell. E aborta em coisas que você esperava falhar (grep que não acha nada retorna 1). Padrões: comando || true quando a falha é aceitável; if ! comando; then ... para tratar; e testar o script de verdade. Não é mágica — é uma rede com furos que ainda vale muito a pena.

6.2 trap — limpeza garantida

tmpdir=$(mktemp -d)
cleanup() {
  rm -rf "$tmpdir"
  echo "limpeza feita"
}
trap cleanup EXIT      # roda cleanup ao sair do script, por qualquer motivo (sucesso, erro, Ctrl+C)
trap 'echo "interrompido na linha $LINENO"' ERR

trap ... EXIT é o "defer/finally" do shell — use para remover temporários, liberar locks, restaurar estado.

6.3 Funções e argumentos

log()  { echo "[$(date +%T)] $*" >&2; }        # log vai para stderr, não polui a saída
die()  { log "ERRO: $*"; exit 1; }

processar() {
  local entrada=$1              # local: não vaza para o escopo global
  local destino=${2:-/tmp/out}  # default
  [[ -f "$entrada" ]] || die "não achei $entrada"
  # ...
}

# dentro de uma função ou do script:
# $1 $2 ...  args posicionais   |   $#  quantidade   |   "$@"  todos (preservando)   |   $0  nome do script
processar "$@"              # SEMPRE "$@" com aspas (não $* nem $@ sem aspas)

6.4 Parsing de opções com getopts

verbose=0; output=""
while getopts "vo:h" opt; do
  case $opt in
    v) verbose=1 ;;
    o) output=$OPTARG ;;
    h) echo "uso: $0 [-v] [-o arquivo] entrada..."; exit 0 ;;
    *) exit 2 ;;
  esac
done
shift $((OPTIND - 1))     # agora "$@" são os argumentos posicionais restantes

6.5 ShellCheck — não negociável

shellcheck script.sh (ou o plugin da IDE) analisa estaticamente e pega: expansão sem aspas, uso de variável não declarada, [ ] vs [[ ]], cd sem checar, glob em contexto errado, comparação de string com -eq, e dezenas de outros. Rode no CI com falha em warnings. Um script que passa no ShellCheck já evitou a maioria dos bugs clássicos.

6.6 Idempotência e quando parar

💡 A regra das 100 linhas

Bash é ótimo para colar comandos e automação linear curta. Quando o script passa de ~100 linhas, ganha estruturas de dados, lida com muitos casos, ou precisa de testes de verdade — reescreva em Python (ou Go). Você vai lutar menos contra quoting, arrays associativos meia-boca e tratamento de erro frágil, e terá testes decentes. Saber quando sair do bash é senioridade.

💼 Mercado de trabalho

Perguntas: "O que set -euo pipefail faz e qual a pegadinha do -e?", "Para que serve trap ... EXIT?" (cleanup garantido), "Por que "$@" e não $*?" (preserva os argumentos como itens separados), "Você usa ShellCheck?" (sim, no CI), "Quando você não usaria bash?" (> ~100 linhas, lógica complexa → Python). Ter um script robusto no portfólio (com header, trap, getopts, passando ShellCheck) impressiona.

✏️ Exercício 6 — Endureça o script

Um script tem: #!/bin/bash na primeira linha e nada mais de config; cd $1; tar czf backup.tar.gz *; scp backup.tar.gz servidor:/backups/; rm backup.tar.gz. Liste 6 melhorias.

Gabarito: (1) #!/usr/bin/env bash + set -euo pipefail + IFS=$'\n\t'. (2) Validar o argumento: dir=${1:?uso: $0 <diretório>} e [[ -d "$dir" ]] || { echo "não é diretório: $dir" >&2; exit 1; }. (3) Aspas: cd "$dir" (e checar o cd: cd "$dir" || exit 1). (4) Arquivo temporário seguro e único: tmp=$(mktemp /tmp/backup.XXXXXX.tar.gz) em vez de backup.tar.gz fixo (evita colisão e race). (5) trap 'rm -f "$tmp"' EXIT para o rm acontecer mesmo se o scp falhar. (6) tar czf "$tmp" -- . (o -- e o . em vez de ** ignora arquivos ocultos e quebra com nomes estranhos). (7) Rodar shellcheck. (8) Considerar rsync em vez de tar+scp se for incremental.

MÓDULO 07 · AVANÇADO

O ambiente: PATH, dotfiles, arquivos de configuração

Objetivo: variáveis de ambiente vs de shell, como o PATH funciona, quais arquivos de config são lidos quando, e como versionar seus dotfiles.

7.1 Variável de shell × variável de ambiente

7.2 PATH

echo $PATH
# /home/ana/.local/bin:/usr/local/bin:/usr/bin:/bin

which python3        # mostra qual executável seria usado
type -a python3      # todos os candidatos (e se é alias/função/builtin)
command -v node      # forma portável de "existe?"

7.3 Quais arquivos de config são lidos (bash)

Tipo de shell
Login (login por SSH, console)/etc/profile → o primeiro que existir entre ~/.bash_profile, ~/.bash_login, ~/.profile
Interativo não-login (abrir um terminal no desktop, novo painel do tmux)/etc/bash.bashrc~/.bashrc
Não-interativo (rodar um script)Nada por padrão (só $BASH_ENV se setada)
💡 O truque padrão

Coloque tudo (aliases, funções, PATH, prompt) no ~/.bashrc, e no ~/.bash_profile ponha só [[ -f ~/.bashrc ]] && source ~/.bashrc. Assim os shells de login também herdam sua config. (No zsh: ~/.zshrc para interativo, ~/.zprofile para login.) source arquivo (ou . arquivo) executa o arquivo no shell atual — é como você "recarrega" o .bashrc sem abrir outro terminal.

7.4 Aliases, funções e histórico

alias ll='ls -lah'
alias gs='git status'
gclone() { git clone "git@github.com:$1.git" && cd "$(basename "$1")"; }   # função aceita argumento; alias não

# histórico
history                # lista
!!                     # último comando; sudo !! = re-roda com sudo
!$                     # último argumento do comando anterior
Ctrl+R                 # busca reversa incremental no histórico (o atalho mais útil do shell)
export HISTCONTROL=ignoredups:erasedups
export HISTSIZE=100000

7.5 Dotfiles versionados

💼 Mercado de trabalho

Perguntas: "Diferença entre variável de shell e de ambiente" (export herda para filhos), "Como o shell resolve um comando?" (procura no PATH, na ordem, primeiro que achar; type -a), ".bashrc vs .bash_profile — quando cada um é lido?" (interativo não-login vs login), "Por que não pôr . no PATH?", "O que source faz diferente de executar o script?" (roda no shell atual, não num filho). Ter dotfiles versionados no GitHub é um ótimo sinal.

✏️ Exercício 7 — Depure o PATH

Você instalou uma versão nova do node em ~/.local/bin, mas node --version ainda mostra a antiga. Como você diagnostica e conserta?

Gabarito: (1) type -a node e which -a node — mostram todos os node no PATH e a ordem; provavelmente o /usr/bin/node (antigo) vem antes de ~/.local/bin. (2) echo "$PATH" | tr ':' '\n' para ver a ordem. (3) Conserta pondo ~/.local/bin na frente: export PATH="$HOME/.local/bin:$PATH" no ~/.bashrc, e source ~/.bashrc (ou abrir novo terminal). (4) hash -r (ou rehash no zsh) para o shell esquecer o caminho cacheado do comando. (5) Confirmar: type node deve apontar para ~/.local/bin/node. Se persistir, checar se há um alias node= ou uma função sombreando (type -a mostra isso).

MÓDULO 08 · AVANÇADO

Ferramentas do dia a dia: ssh, rsync, tmux, systemd

Objetivo: as ferramentas que um dev usa toda semana para trabalhar em máquinas remotas, transferir arquivos, manter sessões, e gerir serviços.

8.1 SSH

ssh-keygen -t ed25519 -C "ana@laptop"        # gera par de chaves (ed25519 > rsa)
ssh-copy-id ana@servidor                     # instala a pública no ~/.ssh/authorized_keys do servidor
ssh ana@servidor                             # login sem senha

# ~/.ssh/config — o arquivo que muda sua vida
Host prod
  HostName 203.0.113.10
  User deploy
  IdentityFile ~/.ssh/id_ed25519
  ForwardAgent yes
Host db-interno
  HostName 10.0.1.5
  ProxyJump prod            # pula pelo bastion "prod" automaticamente
# agora: ssh prod   |   ssh db-interno

8.2 Transferir arquivos: scp e rsync

scp arquivo.tar.gz prod:/tmp/           # cópia simples (bom para 1 arquivo)
scp -r pasta/ prod:/var/www/           # recursivo

rsync -avz --progress ./dist/ prod:/var/www/app/
# -a arquivo (recursivo + permissões + tempos + links)   -v verboso
# -z comprime na transferência   --progress mostra andamento
rsync -avz --delete --exclude='.git' ./ prod:/srv/app/
# --delete: remove no destino o que não existe na origem (espelho — CUIDADO)
# --dry-run: mostra o que faria sem fazer (use SEMPRE antes de --delete)
💡 rsync > scp para quase tudo

rsync só transfere as diferenças (delta), retoma transferências interrompidas, preserva metadados, exclui padrões, e faz --dry-run. A barra final importa: rsync ./dist/ dest/ copia o conteúdo de dist; rsync ./dist dest/ copia a pasta dist para dentro de dest.

8.3 Compressão

tar czf arquivo.tar.gz pasta/     # c criar, z gzip, f arquivo
tar xzf arquivo.tar.gz            # x extrair   |   tar tzf = listar sem extrair
tar xzf arquivo.tar.gz -C /destino
tar caf arquivo.tar.zst pasta/    # a = detecta pela extensão; zstd é rápido e comprime bem
zip -r a.zip pasta/  ;  unzip a.zip

8.4 tmux — sessões que sobrevivem

tmux new -s deploy       # nova sessão nomeada
# ... rode um processo longo ...
Ctrl-b d                 # "detach" — sai deixando tudo rodando
# feche o SSH, o laptop, volte amanhã:
tmux attach -t deploy    # reconecta, tudo como estava
tmux ls                  # lista sessões

Prefixo padrão Ctrl-b: c nova janela, %/" divide painel, o alterna painel, [ modo scroll. É o jeito certo de rodar deploys, migrações e testes longos em servidores — não nohup.

8.5 systemd — serviços no Linux moderno

systemctl status nginx           # estado, PID, últimas linhas de log
systemctl start|stop|restart|reload nginx
systemctl enable --now nginx     # inicia agora E no boot
systemctl list-units --failed    # o que está quebrado

journalctl -u nginx -f           # logs desse serviço, seguindo (como tail -f)
journalctl -u nginx --since "1 hour ago" -p err
journalctl -b                    # logs deste boot
# /etc/systemd/system/minhaapp.service — rodar SUA app como serviço
[Unit]
Description=Minha App
After=network.target

[Service]
ExecStart=/usr/local/bin/minhaapp
Restart=on-failure
User=app
Environment=NODE_ENV=production
WorkingDirectory=/srv/app

[Install]
WantedBy=multi-user.target
# systemctl daemon-reload ; systemctl enable --now minhaapp

systemd timers (.timer + .service) são a alternativa moderna ao cron — com logs no journal, dependências, e OnCalendar= legível. cron (crontab -e) ainda é onipresente e simples para tarefas periódicas.

8.6 O "modern CLI"

ClássicoModerno (mais rápido / melhor UX)
grep -rripgrep (rg) — respeita .gitignore, muito mais rápido
findfd — sintaxe simples, rápido
catbat — syntax highlighting, números de linha
lseza — cores, ícones, --git, árvore
cdzoxidez proj pula para o diretório mais usado que casa
dudust / ncdu — visual, navegável
fzf — fuzzy finder (Ctrl-R turbinado, seleção interativa em qualquer lista); delta — diffs bonitos no git
💼 Mercado de trabalho

Perguntas: "Como você acessa um banco numa rede privada a partir do laptop?" (túnel SSH -L, ou ProxyJump pelo bastion), "scp ou rsync, e por quê?" (rsync — delta, resume, dry-run, exclude), "Como manter um deploy longo rodando se o SSH cair?" (tmux), "Como você vê os logs de um serviço e checa se está rodando?" (systemctl status, journalctl -u ... -f), "cron ou systemd timer?".

✏️ Exercício 8 — Fluxo de deploy manual

Você precisa: conectar a um servidor de produção que só é acessível via um bastion, subir uma nova build (./dist), reiniciar o serviço, e acompanhar os logs — de forma que, se sua conexão cair, o processo não morra. Descreva os comandos.

Gabarito: (1) ~/.ssh/config com Host prod usando ProxyJump bastion. (2) ssh prod e dentro dele tmux new -s deploy (para o processo sobreviver à queda de conexão). (3) De outro terminal local: rsync -avz --delete --dry-run ./dist/ prod:/srv/app/dist/ para conferir, depois sem --dry-run. (4) De volta no tmux do servidor: sudo systemctl restart minhaapp e systemctl status minhaapp para confirmar que subiu. (5) journalctl -u minhaapp -f --since "2 min ago" para acompanhar. (6) Se a conexão cair, ssh prod de novo e tmux attach -t deploy — tudo continua lá. (Idealmente isto seria um pipeline automatizado — ver apostilas de IaC e de CI/CD — mas o fluxo manual é o que salva numa emergência.)

MÓDULO 09 · MUITO AVANÇADO

Rede, disco, performance e segurança

Objetivo: diagnosticar problemas de rede e de disco, ler os sinais de saúde de uma máquina, e as práticas mínimas de segurança de um servidor.

9.1 Rede

ip a                        # interfaces e IPs (substitui o antigo ifconfig)
ip r                        # tabela de rotas (qual gateway)
ss -tlnp                    # portas TCP em LISTEN, com o processo (substitui netstat)
ss -tnp | grep :5432        # quem está conectado ao Postgres
dig +short exemplo.com      # resolução DNS (ou nslookup / host)
getent hosts exemplo.com    # resolução como a app veria (respeita /etc/hosts, nsswitch)
curl -sv https://api/health # -v mostra handshake TLS, headers, redirects
curl -o /dev/null -s -w '%{http_code} %{time_total}s\n' https://api/
nc -zv host 6379            # a porta está aberta? (teste de conectividade cru)
ping -c 3 host ; traceroute host   # alcance e caminho
mtr host                    # ping + traceroute contínuo (ótimo para perda de pacote)

9.2 Disco e I/O

df -h                       # uso por ponto de montagem — "disco cheio?"
df -i                       # uso de INODES — pode encher antes do espaço (muitos arquivos pequenos)
du -sh * | sort -h          # o que ocupa espaço no diretório atual
du -sh /var/* 2>/dev/null | sort -h
ncdu /var                   # navegável — o jeito rápido de achar o vilão
lsblk                       # dispositivos de bloco e montagens
lsof +D /var/log            # quem tem arquivos abertos ali (útil: "apaguei o log mas o disco não liberou")
⚠️ "Apaguei o arquivo mas o espaço não voltou"

No Linux, o espaço de um arquivo só é liberado quando nenhum processo o tem aberto. Se você rm um app.log de 40 GB que o app ainda está escrevendo, o espaço continua ocupado (o inode existe até o processo fechar). lsof | grep deleted mostra esses casos. Solução: reiniciar/sinalizar o processo, ou truncar em vez de apagar (: > app.log ou truncate -s 0 app.log). Configure logrotate para não chegar nisso.

9.3 Saúde da máquina

Comando
uptime / wload average (1, 5, 15 min) — nº médio de processos rodando ou esperando. Compare com o nº de núcleos: load 8 em 8 CPUs = 100%; load 16 em 8 = fila do dobro
free -hMemória. Olhe available, não free — o Linux usa RAM "livre" como cache e a devolve sob pressão
top / htopProcessos por CPU/mem; no top, %wa alto = espera de I/O; si/so em vmstat = swap ativo (ruim)
vmstat 1 / iostat -xz 1 / mpstat -P ALL 1Séries a cada 1s: CPU, I/O, swap, por núcleo (pacote sysstat)
dmesg -T --level=errMensagens do kernel (OOM killer, erros de disco, rede)

O método USE (Brendan Gregg, ver apostila de Observabilidade & SRE): para cada recurso (CPU, memória, disco, rede), cheque Utilization, Saturation, Errors. "Servidor lento" quase sempre é um dos quatro saturado — descubra qual antes de agir.

9.4 Pacotes

9.5 Segurança mínima de um servidor

💼 Mercado de trabalho

Perguntas de troubleshooting (muito comuns em vagas de backend/SRE): "O serviço não responde — como você investiga?" (DNS → rota → porta com ss/nc → TLS com curl -v → app; e systemctl status + journalctl), "O disco encheu — o que você faz?" (df -h, df -i, ncdu/du, checar lsof | grep deleted, logrotate), "O que é load average e como interpretar?", "Por que free mostra pouca memória livre e isso é normal?" (cache; olhe available).

✏️ Exercício 9 — "O servidor está lento"

Chamam você: uma API está com latência alta. Você tem acesso SSH ao servidor. Descreva os primeiros comandos e o raciocínio.

Gabarito: (1) uptime — load average vs nº de núcleos (nproc): fila de CPU? (2) htop / top — qual processo consome CPU/mem; %wa alto sugere I/O; algum processo em swap? (3) free -havailable baixo + si/so em vmstat 1 = pressão de memória / swap. (4) df -h e df -i — disco ou inodes cheios travam escrita. (5) iostat -xz 1%util perto de 100% num disco = I/O saturado. (6) ss -tn state established | wc -l — explosão de conexões? ss -tn | grep TIME-WAIT | wc -l? (7) journalctl -u api --since "15 min ago" -p warning e dmesg -T --level=err — OOM killer? erros de disco? (8) A rede para as dependências (banco, cache): curl -w tempo, mtr. Método USE: achar qual recurso está saturado antes de mexer. Depois, correlacionar com um deploy recente ou pico de tráfego.

MÓDULO 10 · CARREIRA

Mercado de trabalho: roadmap, entrevistas e portfólio

Objetivo: converter o conteúdo dos módulos anteriores em produtividade e aprovação — onde isso aparece no trabalho, roadmap, perguntas de entrevista e o que mostrar.

10.1 Onde isso aparece no trabalho

Não é uma vaga de "shell scripter" — é uma competência transversal que separa quem se vira sozinho de quem depende de outra pessoa para cada tarefa de infra.

10.2 Roadmap de estudo (5–7 semanas)

SemanasFocoPrática
1Filosofia Unix, navegação, FHS, permissões (Módulos 1–2)Viver no terminal por uma semana (sem GUI para arquivos); resolver 20 tarefas com pipes
2Texto: redirecionamento, grep/sed/awk/jq, o toolkit (Módulo 3)Analisar um log real de ~1GB só com CLI; extrair 10 métricas diferentes
3Processos, sinais, job control, exit codes (Módulo 4)Simular e diagnosticar um processo travado; graceful shutdown
4O shell como linguagem: quoting, expansões, arrays, condicionais (Módulo 5)Reescrever 3 tarefas manuais como funções no .bashrc, com quoting correto
5Scripts robustos: set -euo pipefail, trap, getopts, ShellCheck (Módulo 6)Escrever um script de deploy/backup real, idempotente, passando no ShellCheck
6Ambiente + ferramentas: PATH, dotfiles, ssh, rsync, tmux, systemd (Módulos 7–8)Versionar os dotfiles no git; escrever um .service para uma app
7Rede, disco, performance, segurança (Módulo 9)Praticar o fluxo "servidor lento" e "não conecto" numa VM; hardening básico de SSH

10.3 Banco de perguntas de entrevista (com a resposta que aprova)

Júnior/pleno — "Diferença entre aspas simples e duplas no shell"

Aspas simples: literal absoluto, nada é interpretado ('$x' é o texto $x). Aspas duplas: expandem variáveis e $(...), mas não fazem word splitting nem globbing. Sem aspas: expande e ainda quebra em espaços e expande globs — a fonte nº 1 de bugs. Regra: aspas duplas em toda expansão ("$var", "$@").

Pleno — "O que set -euo pipefail faz?"

-e: aborta o script se um comando retornar erro. -u: erro ao usar variável não definida (pega typos). -o pipefail: um pipe é considerado falho se qualquer comando dele falhar, não só o último. É o cabeçalho padrão de script robusto. Pegadinha do -e: não dispara dentro de if/&&/|| e aborta em coisas que você esperava falhar (grep sem match), então ainda é preciso tratar casos com || true ou if ! cmd.

Pleno — "Diferença entre SIGTERM e SIGKILL"

SIGTERM (15, padrão do kill): pede para o processo terminar; ele pode capturar o sinal e fazer cleanup (fechar conexões, salvar estado). SIGKILL (9): força o encerramento imediato; não pode ser capturado nem ignorado, sem cleanup — pode corromper dados. Sempre TERM primeiro, esperar, e KILL só se não morrer. É o que o Kubernetes faz.

Pleno/sénior — "Um comando roda no seu terminal mas falha no CI. Por quê?"

Causas comuns: shell diferente (#!/bin/sh num Alpine sem bash; sintaxe bash-only), variável de ambiente que existe no seu .bashrc mas não no CI (shell não-interativo não lê o .bashrc), PATH diferente, ferramenta não instalada na imagem, working directory diferente, permissões, ou uma expansão sem aspas que quebra com o dado do CI. Diagnóstico: rodar o comando na mesma imagem do CI (docker run), com set -x para ver a expansão.

Sénior — "Como você investiga 'o servidor está lento'?"

Método USE: para cada recurso, checar utilização/saturação/erros. uptime (load vs nº de CPUs), htop (processo, %wa de I/O), free -h + vmstat 1 (memória, swap), df -h/df -i (disco/inodes), iostat -xz 1 (%util de disco), ss -tn (conexões), journalctl/dmesg (OOM, erros), e a latência para as dependências. Achar o recurso saturado antes de agir, e correlacionar com deploy/tráfego.

Sénior — "Quando você para de usar bash?"

Quando o script passa de ~100 linhas, precisa de estruturas de dados (mapas, listas de objetos), muitos casos condicionais, ou testes de verdade. Aí Python (ou Go) evita a luta contra quoting, arrays associativos limitados e tratamento de erro frágil, e dá testes decentes. Bash brilha em automação linear curta e em colar comandos; reconhecer o limite é senioridade.

Armadilha — "É só um script rápido, não precisa de cuidado"

"Scripts rápidos" viram permanentes e rodam em produção. O custo do cuidado é baixo: #!/usr/bin/env bash + set -euo pipefail, aspas nas expansões, validar os argumentos (${1:?}), trap para limpar temporários, mktemp, e passar no ShellCheck. Isso evita o rm -rf com variável vazia, o "apagou o arquivo errado por causa de um espaço no nome", e o "falhou no meio e deixou lixo".

10.4 O que mostrar (portfólio)

  1. Dotfiles no GitHub: .bashrc/.zshrc organizados, .ssh/config (sem chaves!), aliases e funções úteis, com um install.sh idempotente. Sinaliza que você vive no terminal.
  2. Um script de automação real (deploy, backup, setup de ambiente): com header robusto, getopts, trap, logging, idempotente, README, e um badge de ShellCheck passando no CI.
  3. Um "one-liner cookbook": uma coleção comentada de pipelines úteis (análise de log, manipulação de JSON, operações em massa de arquivos) — mostra fluência com o toolkit.
  4. Um .service + timer do systemd para uma app sua, documentado.
  5. Um writeup de troubleshooting: "diagnostiquei X" com os comandos e o raciocínio (rede, disco ou performance).

10.5 Fontes para continuar

🏁 Síntese final da apostila

Quatro ideias sustentam Linux & shell: (1) ferramentas pequenas + texto + pipes resolvem tarefas complexas sem escrever um programa; (2) quoting (aspas duplas em toda expansão) evita 90% dos bugs de script — e o ShellCheck pega o resto; (3) scripts são código: set -euo pipefail, trap, validação, idempotência — e sair para Python quando passar de ~100 linhas; (4) diagnóstico é camada por camada (DNS→rota→porta→TLS→app) e recurso por recurso (USE: CPU, memória, disco, rede). É a competência que faz você não depender de ninguém para operar o que constrói.