Administração Linux

grep, sed e awk: pipelines de processamento de texto que respondem perguntas reais

Intermediário10 min de leituraAtualizado

Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês

Três ferramentas cobrem quase todo o processamento de texto na linha de comando: o grep seleciona linhas, o sed as edita e o awk faz cálculos sobre campos. Este guia estabelece a base comum de expressões regulares, dá a cada ferramenta a profundidade que ela merece, acrescenta os pequenos utilitários que amarram os pipelines e, por fim, responde perguntas realistas sobre o log de um servidor web, da primeira correspondência até um relatório ordenado.

O que você vai aprender

  • Escrever expressões regulares básicas e estendidas e saber qual ferramenta aceita qual dialeto
  • Selecionar, contar e extrair texto com o grep e suas opções mais úteis
  • Editar fluxos com o sed usando endereços, modificadores de substituição e edição in-place
  • Calcular resumos por campo com o awk usando padrões, ações, arrays e blocos BEGIN/END
  • Combinar cut, sort, uniq, tr e join em pipelines que respondem perguntas sobre arquivos de log

Processamento de texto na linha de comando dá um retorno enorme para um investimento pequeno. Em um sistema Linux, todo arquivo de log, toda exportação CSV, todo arquivo de configuração e toda saída de comando é texto, e três programas — grep, sed e awk — cobrem quase tudo o que você vai querer fazer com ele. São estáveis há décadas, estão em todas as máquinas, inclusive no contêiner mais enxuto, e, quando você aprende a combiná-los, para de escrever scripts descartáveis para perguntas que um pipeline de uma linha responde.

Vale deixar a divisão de tarefas clara desde o início, porque pegar a ferramenta errada é o que deixa esses pipelines feios. O grep decide quais linhas. O sed muda o que uma linha diz. O awk trata a linha como campos e sabe contar, somar e lembrar. Este guia monta a base de expressões regulares que eles compartilham, passa por cada um e termina respondendo perguntas realistas sobre o log de acesso de um servidor web.

As expressões regulares por baixo de tudo

Uma expressão regular descreve um conjunto de strings. A sintaxe é quase toda compartilhada, com uma bifurcação importante.

ConstruçãoCasa com
.qualquer caractere único
[abc], [a-z], [^0-9]um caractere de um conjunto, ou fora dele
[[:digit:]], [[:space:]], [[:alpha:]]classes de caracteres POSIX, sensíveis ao locale
^, $início e fim da linha
*zero ou mais ocorrências do item anterior
\+, \? (BRE) ou +, ? (ERE)uma ou mais; zero ou uma
\{2,5\} (BRE) ou {2,5} (ERE)entre duas e cinco repetições
\(…\) (BRE) ou (…) (ERE)um grupo de captura, referenciado depois como \1
`\(BRE) ou|` (ERE)
\b, \<, \>limites de palavra (extensão GNU)

As expressões regulares básicas (BRE) são o padrão do grep e do sed; as estendidas (ERE) são ativadas com -E e são as que o awk usa nativamente. A única diferença é quais caracteres precisam de barra invertida, e o dialeto a adotar como padrão é o ERE, em que (a|b)+ significa exatamente o que parece. Expressões compatíveis com Perl, com lookahead e quantificadores não gulosos, estão disponíveis como grep -P em sistemas GNU, mas não existem no BSD nem no macOS; evite-as em scripts portáveis.

Dois hábitos evitam a maior parte da confusão. Sempre coloque o padrão entre aspas simples, para que o shell não expanda *, $ ou \ antes de a ferramenta vê-los. E lembre-se de que esses motores são gulosos (greedy): .*" vai até a última aspa da linha, não até a primeira. Quando isso for um problema, use uma classe negada: [^"]*" para na primeira. Montar a expressão de forma interativa contra um texto de exemplo, no testador de regex ou com grep --color, é muito mais rápido do que raciocinar sobre ela de cabeça.

grep: escolhendo linhas

grep -i 'timeout' app.log                  # case-insensitive
grep -n 'ERROR' app.log                    # prefix each match with its line number
grep -c 'ERROR' app.log                    # count matching lines, not matches
grep -v '^#' /etc/ssh/sshd_config          # invert: everything that is not a comment
grep -w 'id' schema.sql                    # whole word only: not "uuid" or "identity"
grep -F 'a.b.c' hosts.txt                  # fixed string: no regex interpretation
grep -r --include='*.java' 'TODO' src/     # recursive, filtered by name
grep -l 'password' /etc/myapp/*            # list only the file names that match
grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+' access.log   # print only the matched part

Quatro opções pesam mais que as outras. -E dá a você o dialeto legível. -o imprime só o texto que casou, o que transforma o grep em um extrator em vez de um filtro. -C 3 (ou -A/-B) mostra linhas de contexto ao redor de cada ocorrência, que é como se lê uma stack trace encontrada pelo nome da exceção. E -q não imprime nada e apenas define o código de saída, que é o que um script quer:

if grep -q '^PasswordAuthentication no' /etc/ssh/sshd_config; then
  echo 'password login is disabled'
fi

grep -f patterns.txt data.txt lê vários padrões de um arquivo, um jeito rápido de filtrar uma lista por outra. E, em um arquivo grande, prefixar o comando com LC_ALL=C pode deixá-lo várias vezes mais rápido, porque o motor passa a trabalhar com bytes em vez de caracteres multibyte — seguro sempre que seus dados forem ASCII.

sed: editando um fluxo

O sed lê a entrada linha por linha, aplica um script a cada uma e imprime o resultado. O comando de longe mais comum é a substituição, s/padrão/substituto/modificadores.

sed 's/localhost/db.internal/' app.conf          # first occurrence per line
sed 's/localhost/db.internal/g' app.conf         # every occurrence
sed 's/DEBUG/TRACE/gi' app.log                   # g and i combine
sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/' dates.txt   # reorder with groups
sed 's#/var/www/old#/var/www/new#g' nginx.conf   # any delimiter: # avoids escaping slashes
sed -E 's/(password=)[^ ]+/\1REDACTED/' app.log  # mask a secret before sharing a log

No substituto, & representa a correspondência inteira e \1 … \9 os grupos de captura. O GNU sed acrescenta \U, \L e \E para converter maiúsculas e minúsculas.

Todo comando pode ser limitado a um endereço — um número de linha, um intervalo ou um padrão:

sed -n '10,20p' large.log                        # -n suppresses output; p prints: lines 10-20
sed '1d' data.csv                                # delete the header row
sed '/^$/d' notes.txt                            # delete blank lines
sed '/^#/d; /^$/d' /etc/nginx/nginx.conf         # comments and blanks: two commands, semicolon
sed -n '/BEGIN CERT/,/END CERT/p' bundle.pem     # everything between two patterns
sed '/\[database\]/,/^\[/ s/^port =.*/port = 5433/' app.ini   # substitute only inside a section
sed '$a\ExtraLine' file.txt                      # append after the last line
sed -n '/exception/I{=;p}' app.log               # print line numbers and lines, case-insensitive

Esse último grupo — um intervalo de endereços com uma substituição — é o padrão que torna o sed realmente útil para arquivos de configuração, porque permite mudar uma chave apenas dentro da seção a que ela pertence.

A edição in-place é feita com -i, e a forma segura informa um sufixo de backup:

sed -i.bak -E 's/^#?(PermitRootLogin) .*/\1 no/' /etc/ssh/sshd_config
diff /etc/ssh/sshd_config.bak /etc/ssh/sshd_config

Sempre rode o comando primeiro sem -i e olhe a saída. E saiba que o sed -i grava um arquivo novo e o renomeia por cima do antigo, então o inode muda: o dono pode ser redefinido, hard links se quebram e um processo que mantém o arquivo antigo aberto continua lendo o conteúdo antigo. Para arquivos lidos por um serviço em execução, isso normalmente não é problema; para qualquer coisa mais delicada, edite uma cópia e instale-a explicitamente.

O sed tem um conjunto de comandos maior — y para transliteração, N para puxar a próxima linha, os comandos de hold space h, H, x, G para trabalhar com várias linhas. Eles existem e de vez em quando salvam o dia, mas um programa sed de várias linhas é código que só se escreve, nunca se lê. Quando você precisar do hold space, quase certamente o que você quer é o awk.

awk: campos, padrões e aritmética

O awk divide cada linha de entrada em campos pelos espaços em branco (ou por um separador escolhido por você) e aplica a ela uma sequência de regras pattern { action }. $1 é o primeiro campo, $NF o último, $0 a linha inteira, NR o número do registro e NF a quantidade de campos.

$ awk '{ print $1, $9 }' access.log | head -3
203.0.113.7 200
198.51.100.44 404
203.0.113.7 200

$ awk -F: '$3 >= 1000 { print $1, $6 }' /etc/passwd
alice /home/alice
bob /home/bob

Sem o padrão, a ação roda em todas as linhas; sem a ação, o comportamento padrão é print $0, o que faz de awk '/error/' um substituto funcional do grep.

BEGIN roda antes de qualquer entrada e END depois de toda ela — o lugar natural para cabeçalhos e totais:

awk -F, 'NR > 1 { total += $3; n++ }
         END { printf "%d rows, average %.2f\n", n, total / n }' sales.csv

Os arrays do awk são associativos, indexados por strings, e é aqui que ele deixa as outras ferramentas para trás. Contar ocorrências de qualquer coisa é um programa de duas linhas:

awk '{ count[$1]++ } END { for (ip in count) print count[ip], ip }' access.log | sort -rn | head

Mais algumas construções cobrem a maior parte do uso real:

# Sum bytes served per status code
awk '{ bytes[$9] += $10 } END { for (s in bytes) printf "%s %.1f MB\n", s, bytes[s]/1048576 }' access.log

# Requests slower than one second, with their path
awk '$NF > 1.0 { printf "%6.2fs  %s\n", $NF, $7 }' timing.log

# Field separator that is a regular expression, and output separator
awk -F'[[:space:]]+' -v OFS='\t' '{ print $2, $5 }' report.txt

# Multi-line records: a blank line separates each block
awk -v RS='' -v FS='\n' '/state: failed/ { print $1 }' units.txt

# Pass a shell variable in properly, rather than splicing it into the program text
threshold=500
awk -v t="$threshold" '$3 > t { print }' metrics.tsv

-v é o jeito certo de levar valores do shell para dentro de um programa awk: interpolá-los em uma string entre aspas simples é pedir bugs de aspas e, se o valor vier de fora, injeção de código. Repare também que o awk tem funções de string de verdade — length(), substr(), split(), gsub(), match(), sprintf() — e que o gawk acrescenta strftime() e systime() para lidar com datas. Um programa awk longo o bastante para merecer um arquivo próprio pode ser executado com awk -f report.awk data.txt.

Os coadjuvantes

FerramentaO que faz
cutseleciona campos ou intervalos de bytes: cut -d: -f1,6 /etc/passwd, cut -c1-8
sortordena linhas: -n numérico, -r reverso, -k2,2 por um campo, -u únicas, -h tamanhos legíveis, -t separador
uniqjunta duplicatas adjacentes: -c conta, -d só as duplicadas, -u só as únicas
trtraduz ou apaga caracteres: tr 'A-Z' 'a-z', tr -d '\r', tr -s ' '
wcconta linhas, palavras e bytes: wc -l
head / tailprimeiras ou últimas linhas; tail -f acompanha um arquivo que cresce
pastejunta arquivos lado a lado em colunas
joinjoin relacional de dois arquivos ordenados por um campo em comum
commcompara dois arquivos ordenados: linhas só no primeiro, só no segundo, em ambos
column -talinha saída separada por espaços em colunas legíveis
nl, rev, fold, expandnumeram, invertem, quebram e convertem tabulações em espaços

O que pega todo mundo é o uniq: ele só compara linhas vizinhas, então a fórmula consagrada é sort | uniq -c; uniq sozinho, em uma entrada não ordenada, quase não faz nada — e não avisa. A sequência sort | uniq -c | sort -rn — agrupar, contar, ranquear — é o burro de carga da análise de logs improvisada.

O join merece mais uso do que recebe. Com dois arquivos ordenados pela mesma chave, ele faz a mesma operação que um join de banco de dados, o que é útil para cruzar uma exportação com uma tabela de referência:

sort -t, -k1,1 users.csv > /tmp/u.csv
sort -t, -k1,1 orders.csv > /tmp/o.csv
join -t, -1 1 -2 1 /tmp/u.csv /tmp/o.csv

Se você usa join com frequência em arquivos grandes, é sinal de que esses dados deveriam estar em um banco de dados, onde as ideias de boas práticas de SQL se aplicam de verdade.

Pipelines que respondem perguntas

Pegue um log de acesso padrão no formato combined, em que o campo 1 é o endereço do cliente, o 7 o caminho, o 9 o status e o 10 o tamanho da resposta.

Quais clientes geraram mais requisições?

awk '{ print $1 }' access.log | sort | uniq -c | sort -rn | head -20

Quantas requisições por código de status, e qual a fatia de cada um no total?

awk '{ c[$9]++; n++ } END { for (s in c) printf "%-5s %6d  %5.1f%%\n", s, c[s], 100*c[s]/n }' \
  access.log | sort -k2 -rn

Quais URLs retornam 500, e com que frequência?

awk '$9 == 500 { print $7 }' access.log | sort | uniq -c | sort -rn | head

Como o tráfego varia ao longo das horas? O campo de timestamp tem a forma [07/Sep/2026:14:32:01, então corte pelos dois-pontos:

awk '{ split($4, t, ":"); hour[t[2]]++ }
     END { for (h = 0; h < 24; h++) printf "%02d %s\n", h, hour[sprintf("%02d", h)] + 0 }' access.log

Total de bytes servidos por cliente, do maior para o menor, em megabytes:

awk '{ b[$1] += $10 } END { for (ip in b) printf "%10.2f MB  %s\n", b[ip]/1048576, ip }' \
  access.log | sort -rn | head

Quais erros apareceram hoje que não existiam ontem?

grep -o 'ERROR .*' today.log   | sort -u > /tmp/today
grep -o 'ERROR .*' yesterday.log | sort -u > /tmp/yesterday
comm -23 /tmp/today /tmp/yesterday

Acompanhar um log ao vivo, mantendo só as requisições lentas e mascarando tokens:

tail -F /var/log/app/access.log \
  | awk '$NF > 2.0' \
  | sed -E 's/(token=)[A-Za-z0-9._-]+/\1REDACTED/g'

Repare em tail -F em vez de -f: a letra maiúscula reabre o arquivo depois da rotação de logs, que é exatamente o momento em que você menos quer que o seu monitoramento fique mudo. A mesma distinção importa na discussão sobre rotação em logs e troubleshooting.

Monte esses pipelines aos poucos. Rode a primeira etapa, olhe dez linhas, acrescente a próxima etapa e olhe de novo. Depurar um pipeline de cinco etapas escrito de uma vez é muito mais difícil do que fazê-lo crescer etapa por etapa, e inserir | head -5 enquanto você trabalha mantém o feedback instantâneo mesmo em um arquivo grande.

Escolhendo a ferramenta certa

Uma regra de decisão aproximada:

  • Só precisa saber se algo está presente, ou ver as linhas? grep.
  • Precisa mudar o texto de cada linha, ou apagar ou imprimir um intervalo? sed.
  • Precisa de colunas, aritmética, agrupamento, formatação ou estado entre linhas? awk.
  • Precisa de velocidade em um arquivo enorme? Coloque o grep mais seletivo primeiro, para que as etapas seguintes recebam menos dados, e prefixe com LC_ALL=C.
  • Precisa lidar com dados estruturados — JSON, YAML, XML? Use um parser. jq, yq e xmlstarlet existem justamente porque expressões regulares não conseguem analisar formatos aninhados de forma confiável, e um pipeline que faz grep em JSON vai quebrar no dia em que a ordem dos campos mudar. O mesmo vale para HTML.

Erros comuns

  • Padrões sem aspas. O shell expande * e $ primeiro; coloque todo padrão entre aspas simples.
  • uniq sem sort. Só duplicatas adjacentes são juntadas, então as contagens saem erradas.
  • Um .* guloso engolindo a linha. Use uma classe negada como [^"]* para parar no primeiro delimitador.
  • grep | awk '{print $1}'. O grep é redundante: awk '/pattern/ { print $1 }' faz as duas coisas em uma única passada.
  • sed -i sem backup nem teste a seco. Imprima o resultado primeiro e depois edite com -i.bak.
  • Interpolar variáveis do shell em um programa awk. Passe-as com -v.
  • Analisar JSON ou XML com expressões regulares. Use jq ou um parser de verdade; o aninhamento derrota o motor de regex.
  • Esquecer que cut -f exige um delimitador de um único caractere. Para sequências de espaços, use awk ou normalize antes com tr -s ' '.

Principais conclusões

  • O grep seleciona linhas, o sed as transforma e o awk calcula sobre campos — casar a ferramenta com a pergunta mantém os pipelines curtos.
  • Aprenda expressões regulares estendidas (grep -E, sed -E, awk nativamente) e sempre coloque os padrões entre aspas simples.
  • grep -o, -c, -q, -v e -C transformam um filtro em extrator, contador, teste e visualizador de contexto.
  • Os endereços do sed — números de linha, intervalos e intervalos de padrões — permitem editar exatamente uma seção de um arquivo de configuração; -i.bak depois de um teste a seco mantém a operação segura.
  • Os arrays associativos do awk, com os blocos BEGIN/END, respondem em uma única passada a perguntas de contagem, soma e agrupamento; passe valores do shell com -v.
  • sort | uniq -c | sort -rn é a fórmula consagrada para ranquear; comm e join comparam e cruzam arquivos ordenados.
  • Monte os pipelines uma etapa por vez, coloque o filtro mais seletivo primeiro e troque para um parser de verdade para JSON, XML e HTML.

Fontes e leituras complementares

Perguntas frequentes

Quando usar grep, quando usar sed e quando usar awk?
Use o grep para decidir quais linhas interessam, o sed para transformar linhas com substituições e o awk quando a resposta depende de dividir a linha em campos, fazer contas ou acumular estado entre linhas. Se você se pegar encadeando vários comandos sed com cut e um contador, geralmente tudo isso cabe em um programa awk curto, mais fácil de ler e mais rápido.
Por que minha expressão regular precisa de barras invertidas em uma ferramenta e não em outra?
As expressões regulares básicas, padrão no grep e no sed, tratam o sinal de mais, o ponto de interrogação, as chaves, os parênteses e a barra vertical como caracteres literais e exigem uma barra invertida para dar a eles o significado especial. As expressões regulares estendidas, ativadas com grep -E e sed -E e usadas nativamente pelo awk, fazem o contrário. Use sempre o dialeto estendido e suas expressões ficarão muito mais legíveis.
É seguro rodar sed -i em um arquivo de configuração de produção?
É, desde que você teste antes e guarde um backup. Rode o comando sem -i e leia a saída; depois use -i.bak para que o original seja preservado como arquivo.bak. Observe que o GNU sed substitui o arquivo renomeando uma cópia temporária, então o dono e eventuais hard links podem mudar. Em um arquivo crítico, edite uma cópia, compare com diff e instale o resultado com o dono e as permissões corretos.
Como casar uma tabulação ou lidar com um arquivo com quebras de linha do Windows?
As ferramentas GNU aceitam as sequências de escape \t e \r dentro de expressões regulares, e o awk também as entende em strings. Arquivos vindos do Windows trazem um carriage return no fim de cada linha, que faz o último campo não casar. Remova-os de uma vez com sed -i 's/\r$//' arquivo, ou ajuste o separador de campos para que o awk os ignore, em vez de passar uma hora depurando caracteres invisíveis.