SSD Nodes Learn 🎉 VPS desde $4.99/mês
Guias Matt ConnorPor Matt Connor · Atualizado 2026-08-07

Bash: $() ou crases na substituição de comandos?

Entenda por que $(...) usa um subshell, perde cd e variáveis, por que crases aninham mal e quando usar process substitution para manter o estado.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, August 4, 2026.

O que a substituição de comandos do bash faz

A substituição de comandos do Bash substitui $(command) pelo texto que esse comando escreveu na saída padrão. A sintaxe mais antiga, com crases, faz o mesmo. Tudo o que costuma surpreender resulta de dois factos: o comando é executado num processo separado, chamado subshell, e todas as quebras de linha no fim da saída são removidas.

mkdir -p /tmp/subst-demo
cd /tmp/subst-demo
printf 'alpha\nbeta\ngamma\n' > three.txt
count=$(wc -l < three.txt)
echo "$count"
3

Essa é toda a funcionalidade. wc -l escreveu 3 seguido de uma quebra de linha, a quebra de linha foi removida e count contém os dois caracteres pretendidos. Note que wc -l < three.txt escreve apenas um número porque o GNU wc, ao ler a entrada padrão, não tem um nome de ficheiro para escrever. Escreva wc -l three.txt em vez disso e capture 3 three.txt, que é uma string diferente e uma causa comum de falhas posteriores em operações aritméticas.

O restante deste guia aborda o comportamento que ninguém espera, porque um subshell é um processo separado e um processo separado não pode alterar a shell na qual está a escrever.

Use $() em vez de backticks

As duas formas são válidas. $() faz parte do POSIX, por isso dash, ash e busybox sh suportam-no. Já não existe nenhuma razão de portabilidade para escrever backticks, e há duas razões concretas para não o fazer.

Backticks não permitem aninhamento

echo "$(echo "$(echo hi)")"
echo "`echo `echo hi``"
hi
echo hi

A segunda linha imprimiu as palavras literais echo hi. O shell avança até encontrar o backtick seguinte que não esteja escapado, por isso o segundo backtick que escreveu fechou o primeiro. O comando executado foi, na realidade, echo sem argumentos. Esse comando imprimiu uma linha vazia, cuja quebra de linha foi depois removida, deixando uma cadeia vazia. As palavras echo hi ficaram como texto simples, e o par final de backticks executou um comando vazio.

Para aninhar backticks, tem de escapar cada backtick interno:

echo "`echo \`echo hi\``"
hi

Cada nível adicional duplica novamente o número de escapes necessários. $() não tem este problema, porque o parser corresponde parênteses em vez de procurar um caractere delimitador.

Backticks alteram as barras invertidas antes da execução do comando

echo "$(echo 'a\\b')"
echo "`echo 'a\\b'`"
a\\b
a\b

O mesmo comando interno produziu uma saída diferente. Dentro de backticks, o shell remove uma camada de escapes de barra invertida antes de analisar o texto interno, por isso as aspas simples não protegeram nada. Dentro de $(), o texto entre parênteses é analisado como um script normal, por isso as aspas simples comportam-se como esperado. Isto causa mais problemas em one-liners de sed e awk, nos quais uma barra invertida removida transforma um padrão funcional num padrão diferente sem gerar necessariamente um erro.

As regras de quoting também recomeçam dentro de $(), o que permite aninhar aspas duplas sem as escapar:

path=/etc/nginx/nginx.conf
echo "$(dirname "$path")"
/etc/nginx

A forma equivalente com backticks precisa de \" em torno de $path. Cada escape é uma oportunidade para cometer um erro.

Por que cd dentro de $() não altera o seu shell

Porque $(...) cria um novo processo. O subshell recebe uma cópia das suas variáveis e uma cópia do diretório de trabalho. Ele altera a própria cópia, imprime algo e termina. A cópia desaparece com o processo.

cd /tmp/subst-demo
pwd
target=$(cd /etc && pwd)
echo "$target"
pwd
/tmp/subst-demo
/etc
/tmp/subst-demo

Nada falhou. O cd funcionou, e pwd dentro do subshell realmente imprimiu /etc. Simplesmente não há nenhum caminho para essa alteração voltar, porque as únicas coisas que um subshell entrega ao processo pai são a saída padrão e um status de saída.

As atribuições de variáveis funcionam da mesma forma:

count=0
msg=$(count=99; echo "inside: $count")
echo "$msg"
echo "outside: $count"
inside: 99
outside: 0

A mesma regra explica a versão desse problema que ocorre com muito mais frequência: a que envolve um pipeline em vez de uma substituição:

n=0
cat three.txt | while read -r line; do n=$((n+1)); done
echo "$n"
0

Cada etapa de um pipeline é executada no seu próprio subshell. Portanto, o loop while incrementou uma cópia de n e depois terminou. Substitua o pipe por um redirecionamento para que o loop seja executado no seu shell:

n=0
while read -r line; do n=$((n+1)); done < three.txt
echo "$n"
3

O Bash pode executar a última etapa de um pipeline no shell atual com shopt -s lastpipe, mas apenas quando o controlo de tarefas está desativado, o que nunca acontece num shell interativo. Use o redirecionamento.

Para onde foi o prompt? Comandos interativos dentro de $()

Uma substituição de comando redireciona a saída padrão para um pipe e deixa a entrada padrão intacta. Um programa que escreve a pergunta na saída padrão e depois espera uma resposta perde a pergunta, mas continua à espera. O terminal parece bloqueado.

ask() { printf 'Username: '; read -r u; printf '%s\n' "$u"; }
ask
Username: deploy
deploy

A palavra deploy na primeira linha é o que escreveu. Agora execute a mesma função dentro de uma substituição:

v=$(ask)
deploy

Aparecem apenas as teclas que escreveu, ecoadas pelo controlador do terminal, e não pelo programa. O prompt foi para outro lugar:

echo "$v"
Username: deploy

O prompt está agora dentro da variável, colado à resposta, porque $() capturou tudo o que a função escreveu na saída padrão. A sua introdução de dados continuou a chegar a read, porque a entrada padrão não foi alterada. Esta é a assinatura exata do erro descrito como "o meu script bloqueia e não mostra nada".

Algumas ferramentas escrevem os prompts na saída de erro padrão ou diretamente em /dev/tty para que continuem visíveis. Muitas não o fazem. Se uma função tiver de permanecer dentro de uma substituição, envie o prompt para a saída de erro padrão:

ask() { printf 'Username: ' >&2; read -r u; printf '%s\n' "$u"; }
v=$(ask)
echo "$v"
Username: deploy
deploy

A saída de erro padrão não é capturada. Por isso, o prompt chega ao terminal e apenas a resposta é atribuída a v.

Por que ls fica diferente dentro de $()?

Isso acontece porque ls chama isatty no descritor de ficheiro 1 e altera o formato da saída com base na resposta. Na linha de comandos, esse descritor é o seu terminal, por isso ls distribui os nomes em colunas ao longo da linha. Dentro de uma substituição, ele é um pipe, por isso ls muda para um nome por linha.

mkdir -p /tmp/tty-demo
cd /tmp/tty-demo
touch alpha beta delta gamma
echo "$(ls)"
alpha
beta
delta
gamma

A mesma verificação desativa as cores em grep --color=auto e desativa o paginador em git. Isto é uma funcionalidade. Assim, um script obtém uma saída estável e legível por máquina sem ter de a solicitar explicitamente.

Isto também responde a uma pergunta frequente sobre pipelines. ls | sort na linha de comandos e $(ls | sort) apresentam o mesmo conteúdo, porque ls tinha um pipe na saída nos dois casos. Dentro de uma substituição, o que muda é a última etapa do pipeline. sort nunca verifica se existe um terminal, por isso a sua saída nunca muda. Se colocar um comando que deteta terminais no fim do pipeline, a saída muda. É por isso que um pipeline testado visualmente pode comportar-se de forma diferente assim que é incluído em $().

Há uma advertência importante: não analise a saída de ls num script, embora pareça conveniente. Os nomes de ficheiros podem conter espaços e quebras de linha. Use um glob ou find -print0 com read -d ''.

As quebras de linha finais que desaparecem silenciosamente

A substituição de comandos remove todas as quebras de linha no final da saída. Não apenas a última. Todas.

cd /tmp/subst-demo
printf 'hello\n\n\n' > blanks.txt
wc -c < blanks.txt
v=$(cat blanks.txt)
printf '%s' "$v" | wc -c
8
5

O ficheiro contém hello mais três quebras de linha, num total de 8 bytes. A variável contém hello, num total de 5 bytes. Três bytes desapareceram sem qualquer aviso.

A remoção é intencional e normalmente útil. É o que permite que stamp=$(date -u +%Y%m%dT%H%M%SZ) produza um fragmento de nome de ficheiro utilizável, em vez de um nome que contenha uma quebra de linha. Por isso, o padrão é seguro em algo como um script agendado de backup do restic:

stamp=$(date -u +%Y%m%dT%H%M%SZ)
printf 'backup-%s.tar.gz\n' "$stamp"
backup-20260804T031500Z.tar.gz

O seu timestamp será diferente. O importante é que o nome fique numa única linha.

A consequência é que não pode usar $() para mover os bytes exatos de um ficheiro. Se precisar das quebras de linha finais, acrescente um carácter sentinela dentro da substituição e remova-o depois:

v=$(cat blanks.txt; printf x)
v=${v%x}
printf '%s' "$v" | wc -c
8

O x fica depois das quebras de linha, por isso já não existem quebras de linha finais para remover. Em seguida, ${v%x} remove o sentinela e deixa os bytes originais.

Há dois detalhes relacionados. Para ler um ficheiro inteiro, v=$(<blanks.txt) faz o mesmo trabalho sem executar cat, porque o bash abre o ficheiro diretamente. Também remove as quebras de linha finais da mesma forma. Já as here-strings funcionam no sentido oposto: acrescentam uma quebra de linha que não escreveu:

wc -c <<< 'abc'
4

Coloque o resultado entre aspas, caso contrário o bash fará divisão em palavras e expansão de padrões

Uma substituição sem aspas passa pela divisão em palavras e, em seguida, pela expansão de nomes de caminho. Uma substituição entre aspas não passa por nenhuma das duas.

printf 'a b\tc\nd\n' > words.txt
echo $(cat words.txt)
echo "$(cat words.txt)"
a b c d
a b	c
d

Sem aspas, o bash dividiu a saída nos caracteres de IFS, que, por padrão, são espaço, tabulação e nova linha, e echo juntou as quatro partes com espaços simples. Entre aspas, o texto chegou como uma única palavra, com a tabulação e a nova linha interna intactas.

A expansão de padrões é a parte mais perigosa:

mkdir -p /tmp/glob-demo
cd /tmp/glob-demo
touch one.txt two.txt
printf '*\n' > pattern.txt
p=$(cat pattern.txt)
echo $p
echo "$p"
one.txt pattern.txt two.txt
*

A atribuição em si era segura, porque as atribuições não fazem divisão em palavras nem expansão de padrões. O problema ocorreu em echo $p, onde * foi expandido contra o diretório atual. Um script que lê um padrão de um ficheiro de configuração e se esquece das aspas atuará sem problemas sobre todos os ficheiros que conseguir ver. Coloque todas as expansões entre aspas para eliminar toda esta classe de erros. Omita as aspas apenas quando pretende realmente fazer a divisão, o que é raro.

Por que local x=$(cmd) sempre retorna 0?

Porque local é um comando por si só, e $? informa o status de local, não o status da substituição de comandos que ele contém.

check_bad() { local out=$(false); echo "status: $?"; }
check_bad
status: 0

false terminou com o código 1, local conseguiu declarar a variável, e o 1 foi descartado. declare, export, typeset e readonly têm o mesmo comportamento. set -e também não detetará o erro, porque, do ponto de vista da shell, nada falhou.

Separe a declaração da atribuição:

check_good() { local out; out=$(false); echo "status: $?"; }
check_good
status: 1

Uma atribuição simples no nível superior já informa o status da última substituição de comandos:

out=$(exit 3)
echo $?
3

Isto é especialmente importante numa verificação de integridade executada por um serviço e temporizador do systemd, em que um código de saída mascarado faz com que a unidade informe sucesso em todas as execuções, embora o trabalho que deveria verificar nunca tenha sido executado.

As alternativas dentro da shell que realmente deve usar

A maioria das pessoas recorre a $(...) porque quer os dados numa variável. Muitas vezes, o que realmente pretendem é entrada, não captura. Estas quatro formas mantêm o estado na shell atual.

Um redirecionamento no loop

cd /tmp/subst-demo
while read -r line; do printf 'got: %s\n' "$line"; done < three.txt
got: alpha
got: beta
got: gamma

Não é criado nenhum processo para a entrada, por isso qualquer valor definido pelo corpo do loop permanece disponível depois do loop.

Substituição de processos

while read -r line; do printf 'got: %s\n' "$line"; done < <(sort -r three.txt)
got: gamma
got: beta
got: alpha

<(command) fornece um caminho, como /dev/fd/63, que lê a saída do comando. O comando continua a ser executado no seu próprio processo. O loop while não é executado noutro processo, e esse é precisamente o objetivo. O espaço em < <( é obrigatório: <<( é interpretado como o início de um here-document e não será analisado corretamente. A substituição de processos é uma funcionalidade do bash, por isso um script com #!/bin/sh no Ubuntu ou Debian é executado com dash e falha nesse ambiente. Use #!/bin/bash.

Here-strings

read -r first rest <<< 'alpha beta gamma'
echo "$first"
echo "$rest"
alpha
beta gamma

<<< envia uma string para a entrada padrão de um comando. read é executado na sua shell, por isso ambas as variáveis ficam definidas no local onde as pode utilizar. read -r a b <<< "$(some-command)" é a forma normal de extrair dois campos de uma linha de saída.

mapfile para ficheiros inteiros

mapfile -t lines < three.txt
echo "${#lines[@]}"
echo "${lines[1]}"
3
beta

mapfile, também escrito como readarray, lê um ficheiro para um array na shell atual. -t remove o newline final de cada elemento. Requer bash 4 ou posterior, e o Ubuntu 24.04 inclui bash 5.2, por isso está disponível em qualquer imagem de servidor atual.

Uma lista de verificação antes de confirmar o script

  • Escreva $(command) e coloque-o entre aspas como "$(command)", a menos que pretenda especificamente dividir o conteúdo.
  • Presuma que as novas linhas finais foram removidas. Adicione um caractere sentinela se precisar de as recuperar.
  • Mantenha os comandos interativos fora das substituições ou envie os respetivos prompts para o erro padrão.
  • Escreva local out numa linha própria quando o estado de saída de out=$(command) for relevante.
  • Para definir variáveis a partir da entrada, use um redirecionamento ou uma substituição de processo em vez de um pipe.

Estas estruturas aparecem nos primeiros scripts simples que as pessoas escrevem quando estão a configurar um novo VPS, e as falhas permanecem silenciosas. Um script de backup que capturou um prompt num nome de ficheiro ou uma verificação de integridade que ocultou um código de saída continua a comunicar sucesso. O custo aumenta quando começa a executar o mesmo script em vários servidores, porque a saída que já não lia passa a ser saída que não lê em vinte máquinas.

FAQ

Por que cd dentro de $() não altera o diretório atual?

$(...) executa o comando num subshell, que é um processo separado com uma cópia do diretório de trabalho e das variáveis. cd altera essa cópia. Depois, o processo termina e a cópia é descartada. Um subshell só pode devolver a saída padrão e um status de saída. Por isso, não existe um mecanismo para a alteração do diretório chegar ao shell pai. Se precisar do próprio diretório, capture-o com target=$(cd /etc && pwd) e use "$target". Se quiser que o shell mude de diretório, execute cd diretamente, sem o envolver numa substituição.

Qual é a diferença entre $() e crases no bash?

Produzem o mesmo resultado para comandos simples, mas diferem em dois aspetos importantes. $() permite aninhamento direto, porque o analisador corresponde aos parênteses. As crases exigem uma crase escapada para cada nível de aninhamento. As crases também removem um nível de escape de barra invertida antes de o comando interno ser analisado. Assim, ` echo 'a\\b' prints a\b while $(echo 'a\\b') prints a\\b. $() is in POSIX and works in dash and busybox sh`, pelo que não existe uma justificação de portabilidade para usar crases.

Por que o meu script fica bloqueado, sem apresentar um prompt, quando um comando faz uma pergunta?

A substituição de comandos redireciona a saída padrão para um pipe, mas mantém a entrada padrão ligada ao seu terminal. Um programa que escreve o prompt na saída padrão tem esse prompt capturado na variável, enquanto o read correspondente continua à espera da sua resposta. O terminal mostra apenas os caracteres que escreve, reproduzidos pelo controlador do terminal. Coloque a pergunta fora da substituição ou faça o prompt escrever na saída de erro padrão com printf 'Username: ' >&2, para que não seja capturado.

Por que as linhas vazias no fim da minha variável desapareceram?

A substituição de comandos remove todas as quebras de linha finais, não apenas a última. printf 'hello\n\n\n' > f; v=$(cat f) deixa v com cinco bytes, enquanto o ficheiro contém oito. Para as preservar, acrescente um marcador dentro da substituição e remova-o depois com v=$(cat f; printf x) seguido de v=${v%x}. O marcador fica depois das quebras de linha, pelo que não há nada no fim para o bash remover.

Por que local out=$(cmd) indica sempre sucesso?

local é um comando independente. $?, executado depois dessa linha, indica se local conseguiu declarar a variável. O status de saída da substituição é consumido e descartado. Isto também significa que set -e não interromperá o script. declare, export, typeset e readonly comportam-se da mesma forma. Escreva local out numa linha e out=$(cmd) na linha seguinte. Depois, $? indicará o status real.

#bash#shell-scripting#linux#subshell#coreutils