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).
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)
- Servidores de produção — a esmagadora maioria roda Linux.
- Contêineres — a imagem base (Alpine, Debian, Ubuntu) é Linux; depurar um container é depurar Linux.
- CI/CD — os runners do GitHub Actions / GitLab CI são Linux; quando o pipeline quebra, você lê logs de shell.
- Nuvem — VMs, funções, Kubernetes: Linux por baixo.
- Seu ambiente — macOS é Unix (o shell transfere quase todo); Windows tem o WSL (Windows Subsystem for Linux), praticamente obrigatório para dev hoje.
1.2 A filosofia Unix
- Programas pequenos que fazem uma coisa bem.
grepsó busca.sortsó ordena.wcsó conta. - 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.
- 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
| Termo | O que é |
|---|---|
| Terminal (emulador) | O programa gráfico que mostra o texto e recebe suas teclas (GNOME Terminal, iTerm2, Windows Terminal, Alacritty, Kitty) |
| Shell | O interpretador que lê seus comandos e os executa — o "programa" que roda dentro do terminal |
| bash | O 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 sh | O 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/shUm 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".
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.
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ório | O que guarda |
|---|---|
/ | A raiz; tudo pendura daqui (não há "drive C:") |
/etc | Arquivos de configuração do sistema (texto) |
/var | Dados variáveis: /var/log (logs), /var/lib (estado de serviços), /var/cache |
/usr | Programas e bibliotecas do sistema (/usr/bin, /usr/lib) |
/home/<user> | Diretório pessoal (~); seus arquivos e config de usuário |
/tmp | Temporários; limpo no boot (ou por política). /dev/shm é tmpfs (RAM) |
/opt | Software de terceiros "auto-contido" |
/proc, /sys | Sistemas de arquivos virtuais: estado do kernel e dos processos (/proc/<pid>) |
/dev | Arquivos 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 lixeirarm 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)
*= qualquer sequência (menos/e, por padrão, arquivos ocultos);?= um caractere;[abc]= um dos listados;{png,jpg}= brace expansion (não é glob, é lista literal).- O shell expande o glob antes de passar ao comando —
rm *.txtvirarm a.txt b.txt. Se nenhum arquivo casa, bash por padrão passa o*.txtliteral (cuidado;shopt -s nullglobmuda isso). **(globstar,shopt -s globstar) = recursivo.
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
| Bit | Em arquivo | Em diretório |
|---|---|---|
| r (4) | ler o conteúdo | listar os nomes (ls) |
| w (2) | modificar o conteúdo | criar/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
- Bits especiais: setuid (roda com o dono do arquivo — usado com cuidado, ex.:
passwd), setgid (em diretório: arquivos herdam o grupo), sticky (/tmp: só o dono apaga seu arquivo). - Não confunda "não consigo apagar este arquivo (só leitura)" — o que importa é o
wno diretório, não no arquivo. sudoexecuta um comando como outro usuário (padrão: root). Configurado em/etc/sudoers. Princípio: use o mínimo necessário.
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.
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
| Descritor | Nº | Redireciona com |
|---|---|---|
| stdin (entrada) | 0 | < arquivo |
| stdout (saída normal) | 1 | > arquivo (sobrescreve), >> arquivo (anexa) |
| stderr (erros) | 2 | 2> 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
> arquivo 2>&1A 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
cat(concatena/mostra),less(paginador —/busca,Gvai ao fim,F= "follow" como tail -f),head -n 20,tail -n 50,tail -f app.log(segue em tempo real),tail -F(segue mesmo com rotação).wc -l(linhas),-w(palavras),-c(bytes).
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
| Comando | Faz |
|---|---|
cut -d',' -f2,4 | Extrai colunas por delimitador (ou -c por posição) |
sort / sort -n / sort -rn / sort -u / sort -k2 | Ordena (numérico, reverso, único, por coluna) |
uniq -c / uniq -d | Colapsa duplicatas adjacentes (precisa de sort antes); -c conta |
tr 'a-z' 'A-Z' / tr -d ' ' / tr -s ' ' | Traduz/apaga/comprime caracteres |
xargs | Transforma stdin em argumentos: find . -name '*.js' | xargs wc -l. Use -0 com find -print0 para nomes com espaço; -P4 paraleliza |
tee arquivo | Escreve na tela E num arquivo ao mesmo tempo |
paste, join, comm | Combinar 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.
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).
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
| Sinal | Nº | O que faz |
|---|---|---|
| SIGTERM | 15 | "Termine, por favor" — o processo pode fazer cleanup. O padrão do kill e o que orquestradores mandam primeiro |
| SIGKILL | 9 | "Morra agora" — não pode ser capturado nem ignorado; sem cleanup. Último recurso |
| SIGINT | 2 | Ctrl+C — interrupção pelo usuário |
| SIGHUP | 1 | Terminal fechou; muitos daemons usam para "recarregar config" |
| SIGSTOP / SIGCONT | 19 / 18 | Pausar / 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
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
- Todo comando retorna um exit code:
0= sucesso, qualquer outro (1–255) = erro. Consulte comecho $?. cmd1 && cmd2— rodacmd2só secmd1teve sucesso.cmd1 || cmd2— rodacmd2só secmd1falhou (fallback).cmd1 ; cmd2— roda os dois independentemente.timeout 30 ./lento.sh— mata o comando se passar de 30s (exit 124).watch -n 5 'df -h /'— re-executa a cada 5s.- Num pipe, o exit code é o do último comando (por isso
set -o pipefail— Módulo 6).
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).
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
| Forma | O que acontece dentro |
|---|---|
Sem aspas $VAR | Expande 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
"$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
| Teste | Verdadeiro se… |
|---|---|
-e path / -f / -d / -L | existe / é arquivo comum / é diretório / é symlink |
-r / -w / -x | legível / gravável / executável |
-s path | existe e não está vazio |
-z str / -n str | string vazia / string não vazia |
str1 == str2 / != | comparação de string (em [[ ]], o lado direito pode ter glob) |
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) 3 — set -- sem aspas fez word splitting de $x em 3 argumentos posicionais; $# é a contagem. (d) a-b-c — substitui todos os espaços por -. (e) default — z não existe, então ${z:-default} devolve default (sem atribuir a z).
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 armadilhasset -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
- Idempotente: rodar duas vezes tem o mesmo efeito.
mkdir -p(não falha se existe),ln -sfn, checar antes de criar,rsyncem vez decpcego. Scripts de setup/deploy devem ser idempotentes. - mktemp para arquivos temporários (nome único, sem race, sem colisão) — nunca
/tmp/meuarquivo. - Locking (
flock) se o script não pode rodar duas vezes ao mesmo tempo (cron).
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.
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.
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
- Uma variável de shell (
x=1) existe só no shell atual.export xa promove a variável de ambiente, herdada por todo processo-filho. envlista as de ambiente;setlista todas (shell + ambiente + funções).x=1 comando— definexsó para aquela invocação.- Variáveis clássicas:
PATH,HOME,USER,SHELL,PWD,LANG/LC_*,TERM,EDITOR,TZ.
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?"
- Quando você digita
foo, o shell procurafooem cada diretório doPATH, na ordem, e usa o primeiro. Por isso um./node_modules/.binou~/.local/binno início "sombreia" o do sistema. - Adicionar ao PATH (no arquivo de config):
export PATH="$HOME/.local/bin:$PATH". - Nunca coloque
.(diretório atual) no PATH — risco de segurança (rodar um binário malicioso aocdnum diretório).
7.3 Quais arquivos de config são lidos (bash)
| Tipo de shell | Lê |
|---|---|
| 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) |
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
- Seus arquivos de config (
.bashrc,.gitconfig,.tmux.conf,.vimrc,.ssh/config...) num repositório git — reprodutível numa máquina nova em minutos. - Ferramentas: um repo "bare" com alias, GNU Stow (symlinks), chezmoi (templates, segredos, multi-máquina), yadm.
- Nunca commite segredos (chaves privadas, tokens) — use
.gitignore, ou o suporte a segredos do chezmoi, ou um cofre.
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).
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
- ssh-agent guarda a chave descriptografada na memória:
eval "$(ssh-agent)"; ssh-add(uma vez por sessão). - Port forwarding:
ssh -L 5432:localhost:5432 prod— acessa o Postgres do servidor como se fosselocalhost:5432na sua máquina (túnel local).-Rfaz o inverso (remoto → local);-Dcria um proxy SOCKS. - Rodar um comando e sair:
ssh prod 'df -h /'. - Hardening do servidor (Módulo 9): desabilitar login por senha e login de root, mudar/filtrar a porta,
fail2ban.
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 tudorsync 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ássico | Moderno (mais rápido / melhor UX) |
|---|---|
grep -r | ripgrep (rg) — respeita .gitignore, muito mais rápido |
find | fd — sintaxe simples, rápido |
cat | bat — syntax highlighting, números de linha |
ls | eza — cores, ícones, --git, árvore |
cd | zoxide — z proj pula para o diretório mais usado que casa |
du | dust / ncdu — visual, navegável |
| — | fzf — fuzzy finder (Ctrl-R turbinado, seleção interativa em qualquer lista); delta — diffs bonitos no git |
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.)
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)
- Ordem de diagnóstico de "não conecto ao serviço X": resolve o DNS? (
dig) → há rota? (ping/ip r) → a porta responde? (nc -zv) → o TLS negocia? (curl -v) → a app responde certo? (corpo/HTTP status). Vá camada por camada. /etc/hostssobrepõe o DNS — útil para testar, fonte de bug quando esquecido.tcpdump -i any port 5432 -c 20(com sudo) para ver os pacotes quando nada mais explica.
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")
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 | Lê |
|---|---|
uptime / w | load 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 -h | Memória. Olhe available, não free — o Linux usa RAM "livre" como cache e a devolve sob pressão |
top / htop | Processos 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 1 | Séries a cada 1s: CPU, I/O, swap, por núcleo (pacote sysstat) |
dmesg -T --level=err | Mensagens 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
- Debian/Ubuntu:
apt update,apt install,apt list --installed,dpkg -l,apt-cache policy pkg. - RHEL/Fedora:
dnf install,rpm -qa. Arch:pacman -S. Alpine:apk add. - Universais:
brew(macOS/Linux),snap/flatpak,mise/asdf(runtimes de linguagem por projeto). - Por que contêineres: em vez de instalar dependências na máquina (e brigar com versões e drift), a imagem carrega o ambiente exato. Um dev moderno instala pouca coisa na máquina base.
9.5 Segurança mínima de um servidor
- SSH: só chave (
PasswordAuthentication no), sem login de root (PermitRootLogin no),fail2bancontra brute force, e — melhor ainda — SSH só via bastion ou VPN, não exposto à internet. - Menor privilégio: a app roda como um usuário dedicado sem shell (
useradd -r -s /usr/sbin/nologin app), com acesso só ao que precisa.sudogranular, não "sudo para tudo". - Firewall:
ufw/firewalld/ security groups da nuvem — só as portas necessárias. - Atualizações de segurança automáticas (
unattended-upgrades). - Segredos: variáveis de ambiente são melhores que hardcode, mas aparecem em
/proc/<pid>/environ, emps auxe, e podem vazar em logs/crash dumps. Para segredos sérios: um cofre (Vault), arquivos com permissão600lidos em runtime, ou os secrets do orquestrador. Nunca commite; nunca passe na linha de comando (fica nohistorye nops). - No shell: um espaço antes do comando (
export TOKEN=...) o mantém fora do history seHISTCONTROL=ignorespace; ainda assim, prefira não digitar segredos.
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 -h — available 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.
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
- Backend: depurar o container, ler logs em produção, escrever o entrypoint/healthcheck, o Dockerfile, os scripts de seed/migração.
- DevOps / Platform / SRE: o dia inteiro — scripts de automação, systemd, troubleshooting, o "cola" entre ferramentas.
- Dados: pipelines,
cron, mover arquivos, inspecionar dumps,jq/awkantes de subir para o warehouse. - Qualquer dev: o CI falhou — a mensagem é de shell; o build é um script; o "funciona na minha máquina" costuma ser diferença de shell/ambiente.
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)
| Semanas | Foco | Prática |
|---|---|---|
| 1 | Filosofia 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 |
| 2 | Texto: redirecionamento, grep/sed/awk/jq, o toolkit (Módulo 3) | Analisar um log real de ~1GB só com CLI; extrair 10 métricas diferentes |
| 3 | Processos, sinais, job control, exit codes (Módulo 4) | Simular e diagnosticar um processo travado; graceful shutdown |
| 4 | O shell como linguagem: quoting, expansões, arrays, condicionais (Módulo 5) | Reescrever 3 tarefas manuais como funções no .bashrc, com quoting correto |
| 5 | Scripts robustos: set -euo pipefail, trap, getopts, ShellCheck (Módulo 6) | Escrever um script de deploy/backup real, idempotente, passando no ShellCheck |
| 6 | Ambiente + ferramentas: PATH, dotfiles, ssh, rsync, tmux, systemd (Módulos 7–8) | Versionar os dotfiles no git; escrever um .service para uma app |
| 7 | Rede, 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)
- Dotfiles no GitHub:
.bashrc/.zshrcorganizados,.ssh/config(sem chaves!), aliases e funções úteis, com uminstall.shidempotente. Sinaliza que você vive no terminal. - 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. - 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.
- Um
.service+ timer do systemd para uma app sua, documentado. - Um writeup de troubleshooting: "diagnostiquei X" com os comandos e o raciocínio (rede, disco ou performance).
10.5 Fontes para continuar
- Referência: The Linux Command Line (William Shotts, gratuito online); Wicked Cool Shell Scripts; o Bash Guide do "Greg's Wiki" (BashFAQ, BashPitfalls — leitura essencial sobre os erros comuns);
man bashehelppara builtins. - Ferramentas: a documentação do ShellCheck (cada código de erro tem explicação);
tldr(exemplos curtos por comando);explainshell.com(cola um comando, explica cada parte). - Performance: os materiais de Brendan Gregg (o método USE, "Linux Performance").
- Prática: "Bandit" (OverTheWire) para praticar shell num jogo; "Command Line Challenge"; resolver tarefas reais só no terminal por um tempo.
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.