A substituição de comandos é um recurso fundamental em shell script que permite capturar a saída de um comando e utilizá-la como parte de outro comando, em uma variável ou em uma expressão. Isso possibilita criar scripts dinâmicos que reagem a informações obtidas em tempo de execução, como listar arquivos, obter datas ou processar resultados de consultas.

Existem duas formas de realizar substituição de comandos no Bash: a sintaxe moderna $(comando) e a sintaxe legada com backticks (`comando`). Embora ambas funcionem, a primeira é preferível por ser mais clara, permitir aninhamento fácil e ter comportamento mais previsível. Nesta aula, veremos detalhadamente cada uma, como aninhar substituições e diversos casos de uso práticos.

$(comando)

A sintaxe $(comando) é a forma moderna e recomendada de substituição de comandos no Bash. Ela executa o comando dentro dos parênteses e substitui toda a expressão pela saída do comando (stdout). Essa sintaxe é mais legível, especialmente quando o comando contém parênteses ou aspas, e permite aninhamento sem ambiguidade.

Exemplo básico: armazenar a data atual em uma variável.

#!/bin/bash
data_atual=$(date +%d/%m/%Y)
echo "Hoje é $data_atual"

Outro exemplo: contar o número de arquivos no diretório atual.

#!/bin/bash
num_arquivos=$(ls -1 | wc -l)
echo "Existem $num_arquivos arquivos no diretório atual."

Note que a substituição de comandos pode ser usada diretamente em argumentos de outros comandos, sem necessidade de variável intermediária:

#!/bin/bash
echo "O diretório home é: $(echo $HOME)"

Backticks (legado)

Antes da introdução de $(...), a substituição de comandos era feita com backticks (`comando`). Embora ainda funcione em shells modernos, seu uso é desencorajado por diversos motivos: difícil aninhamento, problemas com caracteres especiais (como aspas e escapes) e menor legibilidade.

Exemplo equivalente ao anterior com backticks:

#!/bin/bash
data_atual=`date +%d/%m/%Y`
echo "Hoje é $data_atual"

O principal problema com backticks surge ao tentar aninhar substituições. Por exemplo, para obter a saída de um comando que contém backticks, é necessário escapar os backticks internos com barra invertida, o que torna o código confuso e propenso a erros. Além disso, a interpretação de aspas dentro de backticks é inconsistente entre shells. Por esses motivos, a sintaxe $(...) é sempre preferível.

Aninhamento

Uma das grandes vantagens da sintaxe $(...) é a facilidade de aninhar substituições de comandos. Isso significa que podemos usar o resultado de uma substituição como parte de outra, sem necessidade de escapes complexos.

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

#!/bin/bash
pai=$(basename $(dirname $(pwd)))
echo "O diretório pai é: $pai"

Neste exemplo, $(pwd) retorna o caminho completo, $(dirname ...) extrai o diretório pai e $(basename ...) pega apenas o nome final. O Bash processa de dentro para fora: primeiro $(pwd), depois $(dirname ...), e por último $(basename ...).

Com backticks, o mesmo aninhamento exigiria escapes:

#!/bin/bash
pai=`basename \`dirname \`pwd\`\``
echo "O diretório pai é: $pai"

Como se vê, a legibilidade fica comprometida. Portanto, para aninhamentos, sempre use $(...).

Casos de uso

A substituição de comandos é amplamente utilizada em scripts para capturar informações do sistema, processar listas de arquivos, gerar nomes dinâmicos e integrar com outras ferramentas. Abaixo, alguns casos comuns.

1. Obter informações do sistema:

#!/bin/bash
kernel=$(uname -r)
echo "Versão do kernel: $kernel"

usuarios=$(who | wc -l)
echo "Usuários logados: $usuarios"

2. Processar listas de arquivos:

#!/bin/bash
# Listar apenas arquivos .txt
txt_files=$(ls *.txt 2>/dev/null)
echo "Arquivos .txt:"
echo "$txt_files"

3. Gerar nomes de arquivos com timestamp:

#!/bin/bash
timestamp=$(date +%Y%m%d_%H%M%S)
nome_arquivo="backup_$timestamp.tar.gz"
echo "Criando arquivo: $nome_arquivo"
# tar -czf "$nome_arquivo" /dados

4. Uso em condições:

#!/bin/bash
if [ "$(whoami)" = "root" ]; then
    echo "Você é root."
else
    echo "Você não é root."
fi

5. Combinar com outros comandos:

#!/bin/bash
# Contar linhas de um arquivo específico
linhas=$(wc -l < /etc/passwd)
echo "O arquivo /etc/passwd tem $linhas linhas."

Boas práticas

  • Sempre utilize $(comando) em vez de backticks, a menos que esteja mantendo scripts legados.
  • Aspas duplas ao redor da substituição ("$(comando)") preservam espaços e quebras de linha, evitando problemas com word splitting.
  • Evite substituições desnecessárias dentro de comandos que já aceitam pipes ou redirecionamentos; às vezes um pipe é mais claro.
  • Para comandos que produzem saída longa, considere armazenar em uma variável e depois processar com loops ou ferramentas como sed e awk.

Referências

Exercícios

  1. Crie um script que armazene na variável data a data atual no formato "AAAA-MM-DD" e exiba uma mensagem: "Hoje é AAAA-MM-DD".

    ✓ Resposta:
    #!/bin/bash
    data=$(date +%Y-%m-%d)
    echo "Hoje é $data"
  2. Escreva um comando que exiba o número de diretórios (não arquivos) no diretório atual. Use substituição de comandos e o comando ls -d */.

    ✓ Resposta:
    #!/bin/bash
    num_dirs=$(ls -d */ 2>/dev/null | wc -l)
    echo "Número de diretórios: $num_dirs"
  3. Utilizando aninhamento de substituição de comandos, crie um script que exiba o nome do diretório atual (apenas o nome, não o caminho completo).

    ✓ Resposta:
    #!/bin/bash
    nome_dir=$(basename $(pwd))
    echo "Você está no diretório: $nome_dir"
  4. Escreva um script que verifique se o usuário atual é "root" usando substituição de comandos e exiba uma mensagem apropriada.

    ✓ Resposta:
    #!/bin/bash
    if [ "$(whoami)" = "root" ]; then
        echo "Você é root."
    else
        echo "Você não é root."
    fi
  5. Crie um script que defina uma variável arquivo com o nome do primeiro arquivo .txt encontrado no diretório atual (use ls *.txt 2>/dev/null | head -1) e exiba seu conteúdo com cat. Se não houver arquivo .txt, exiba uma mensagem de erro.

    ✓ Resposta:
    #!/bin/bash
    arquivo=$(ls *.txt 2>/dev/null | head -1)
    if [ -n "$arquivo" ]; then
        cat "$arquivo"
    else
        echo "Nenhum arquivo .txt encontrado."
    fi