A substituição de comandos é um recurso fundamental em shell script que permite executar um comando e utilizar sua saída como parte de outro comando, atribuição de variável ou expressão. Em vez de apenas exibir o resultado na tela, você pode capturá-lo e armazená-lo para processamento posterior. Essa técnica é amplamente usada para criar scripts dinâmicos e automatizados.

Existem duas formas de realizar substituição de comandos: a sintaxe moderna com $(comando) e a sintaxe legada com backticks (`comando`). Embora ambas funcionem, a primeira é recomendada por ser mais legível e permitir aninhamento fácil. Nesta aula, exploraremos cada uma, suas diferenças e como utilizá-las em situações reais.

$(comando)

A sintaxe $(comando) é a forma moderna e preferida de substituição de comandos em bash. Ela executa o comando dentro dos parênteses e substitui toda a expressão pelo resultado da execução. O comando pode ser qualquer comando shell válido, incluindo pipelines, redirecionamentos e até outras substituições.

Essa sintaxe é mais clara, especialmente quando aninhada, pois os parênteses são visualmente distintos. Além disso, o bash interpreta corretamente aspas e caracteres especiais dentro de $(), reduzindo erros. Veja um exemplo simples:

#!/bin/bash
# Captura a data atual
data_atual=$(date)
echo "Hoje é: $data_atual"

No exemplo acima, o comando date é executado e sua saída (a data atual) é atribuída à variável data_atual. Depois, a variável é usada em um echo.

Outro exemplo comum é usar a saída de um comando como argumento de outro:

#!/bin/bash
# Lista arquivos .txt e conta as linhas de cada um
for arquivo in $(ls *.txt); do
    wc -l "$arquivo"
done

Atenção: esse loop pode quebrar se nomes de arquivos contiverem espaços. Para maior segurança, use while read ou find com -print0.

Backticks (legado)

A sintaxe com backticks (`comando`) é a forma original de substituição de comandos, herdada do shell Bourne. Ela ainda funciona no bash, mas é considerada legada e seu uso é desencorajado em scripts modernos. O problema principal é a legibilidade: backticks são facilmente confundidos com aspas simples e dificultam o aninhamento.

Exemplo equivalente ao anterior:

#!/bin/bash
data_atual=`date`
echo "Hoje é: $data_atual"

Quando você precisa aninhar substituições, os backticks exigem escapes com barra invertida, tornando o código confuso:

# Aninhamento com backticks (legado)
resultado=`echo \`whoami\``
echo "Usuário: $resultado"

Já com $(), o mesmo aninhamento fica mais limpo:

resultado=$(echo $(whoami))
echo "Usuário: $resultado"

Por esses motivos, sempre prefira $(comando) em novos scripts.

Aninhamento

O aninhamento de substituições de comandos ocorre quando uma substituição é colocada dentro de outra. Isso é útil para processar saídas em múltiplas etapas. Com $(), o aninhamento é natural e não requer escapes especiais.

Exemplo: obter o nome do diretório pai do diretório atual.

#!/bin/bash
# Caminho completo do diretório atual
caminho_atual=$(pwd)
# Nome do diretório pai (último componente do caminho)
dir_pai=$(basename $(dirname "$caminho_atual"))
echo "Diretório pai: $dir_pai"

Note que dirname retorna o caminho do diretório pai, e basename extrai apenas o nome final. O aninhamento evita variáveis intermediárias.

Outro exemplo: contar quantos usuários estão logados no sistema.

#!/bin/bash
num_usuarios=$(who | wc -l)
echo "Usuários logados: $num_usuarios"

Aqui, who lista os usuários e wc -l conta as linhas. Tudo dentro de $().

Para aninhamentos mais complexos, você pode usar substituições dentro de substituições, como em:

#!/bin/bash
# Pega o primeiro PID do processo bash
pid=$(ps aux | grep bash | head -1 | awk '{print $2}')
echo "PID do primeiro bash: $pid"

Embora funcione, lembre-se de que o grep pode encontrar a si mesmo; use grep [b]ash para evitar.

Casos de uso

A substituição de comandos é usada em inúmeras situações. Abaixo, alguns casos comuns em scripts do dia a dia.

  • Atribuição de variáveis dinâmicas: armazenar saídas de comandos como data, nome do usuário, caminhos etc.
  • Construção de argumentos: usar a saída de um comando como argumento para outro, por exemplo, vim $(find . -name "*.txt").
  • Laços de repetição: iterar sobre a saída de um comando, como listar arquivos ou processos.
  • Testes condicionais: verificar se um comando produziu saída, por exemplo, if [ -n "$(comando)" ].
  • Geração de nomes de arquivos: criar nomes baseados em data/hora, como backup_$(date +%Y%m%d).tar.gz.

Exemplo prático: um script que faz backup de um diretório e nomeia o arquivo com a data.

#!/bin/bash
origem="/home/usuario/documentos"
destino="/backups"
arquivo="backup_$(date +%Y%m%d_%H%M%S).tar.gz"
tar -czf "$destino/$arquivo" "$origem"
echo "Backup criado: $destino/$arquivo"

Outro exemplo: verificar se um serviço está rodando.

#!/bin/bash
servico="apache2"
if systemctl is-active --quiet "$servico"; then
    echo "$servico está ativo"
else
    echo "$servico não está ativo"
fi

Note que aqui usamos o status de saída do comando, não a substituição. Mas poderíamos capturar a saída com $(systemctl is-active "$servico") e comparar com "active".

Boas práticas e observações finais

  • Prefira $(comando) a backticks em scripts novos.
  • Ao usar substituição em loops, cuidado com espaços em nomes de arquivos. Considere usar while IFS= read -r com find.
  • Evite substituições desnecessárias; às vezes, a saída do comando pode ser usada diretamente.
  • Para comandos que produzem saída longa, avalie se precisa armazenar tudo em memória.
  • Lembre-se de que a substituição de comandos remove as novas linhas finais, a menos que esteja dentro de aspas duplas.

Referências

Exercícios

  1. Escreva um script que capture a saída do comando whoami e a exiba em uma mensagem.

    ✓ Resposta:
    #!/bin/bash
    usuario=$(whoami)
    echo "O usuário atual é: $usuario"
  2. Crie um script que armazene a lista de arquivos .txt do diretório atual em uma variável e depois exiba cada nome. Use substituição de comandos.

    ✓ Resposta:
    #!/bin/bash
    arquivos=$(ls *.txt 2>/dev/null)
    for arq in $arquivos; do
        echo "Arquivo: $arq"
    done

    Nota: Esta abordagem falha se nomes contiverem espaços. Versão mais segura:

    #!/bin/bash
    while IFS= read -r -d '' arq; do
        echo "Arquivo: $arq"
    done < <(find . -maxdepth 1 -name "*.txt" -print0)
  3. Escreva um script que use aninhamento de substituição para obter o nome do diretório home do usuário root (dica: use getent passwd root | cut -d: -f6).

    ✓ Resposta:
    #!/bin/bash
    home_root=$(getent passwd root | cut -d: -f6)
    echo "Diretório home do root: $home_root"

    Aninhamento não é necessário aqui, mas poderíamos fazer:

    home_root=$(echo $(getent passwd root | cut -d: -f6))
  4. Crie um script que verifique se o comando curl está instalado. Use substituição de comandos para capturar a saída de which curl e teste se a variável não está vazia.

    ✓ Resposta:
    #!/bin/bash
    curl_path=$(which curl 2>/dev/null)
    if [ -n "$curl_path" ]; then
        echo "curl está instalado em: $curl_path"
    else
        echo "curl não está instalado"
    fi
  5. Escreva um script que exiba a data e hora atuais no formato "AAAA-MM-DD HH:MM:SS" usando substituição de comandos e o comando date.

    ✓ Resposta:
    #!/bin/bash
    data_hora=$(date '+%Y-%m-%d %H:%M:%S')
    echo "Data e hora: $data_hora"