Aceitando Entrada do Usuário com Segurança: Técnicas Essenciais para o Comando read do Bash
Aprenda a aceitar entrada do usuário de forma segura e eficiente em scripts Bash usando o comando `read`. Este guia aborda técnicas essenciais para solicitar entrada, lidar com senhas silenciosamente com `-s`, definir timeouts com `-t` e realizar validação e sanitização básicas de entrada para criar scripts interativos mais robustos e seguros.
Aceitando Entrada do Usuário com Segurança: Técnicas Essenciais para o Comando read do Bash
O comando read do Bash parece inofensivo até que o valor coletado seja usado em um caminho de arquivo, um argumento de comando ou um prompt de senha. A maioria dos problemas não vem do read em si. Vem de confiar no texto cedo demais, esquecer que espaços e metacaracteres do shell são entrada normal do usuário, ou deixar um script travado para sempre porque ninguém respondeu ao prompt.
Um bom script Bash interativo trata a entrada como texto não confiável. Ele pergunta claramente, lê com cuidado, valida antes de agir e mantém segredos fora dos logs. Isso parece formal, mas a versão do dia a dia é simples: coloque variáveis entre aspas, use IFS= read -r por padrão, verifique o status de retorno e rejeite valores que você não sabe como tratar.
Comece com o padrão mais seguro
Para a maioria dos prompts de linha única, este é o padrão que eu uso:
printf 'Project name: '
IFS= read -r project_name
if [[ -z $project_name ]]; then
printf 'Project name is required.\n' >&2
exit 1
fi
Há dois detalhes que vale a pena manter. IFS= impede que o Bash remova espaços em branco no início e no final durante a leitura. -r diz ao read para não tratar barras invertidas como caracteres de escape. Sem -r, alguém digitando C:\Users\me ou uma string contendo \n pode não receber de volta o texto exato que digitou.
Você também pode usar -p para um prompt:
IFS= read -r -p 'Environment [dev/staging/prod]: ' env_name
Isso é bom para um terminal interativo. Ainda uso printf quando quero que o prompt e a leitura sejam mais fáceis de testar separadamente, ou quando preciso de hábitos de portabilidade mais rígidos em relação à formatação da saída.
Verifique se o read realmente foi bem-sucedido
read retorna um status. Use-o. Uma leitura falha pode significar fim de arquivo, um tempo limite ou um terminal interrompido. Se a próxima linha do seu script assumir que a variável é significativa, você pode acidentalmente executar com um valor antigo ou uma string vazia.
if ! IFS= read -r -p 'Deploy tag: ' tag; then
printf 'No input received. Aborting.\n' >&2
exit 1
fi
Isso é importante em scripts que às vezes são executados por uma pessoa e às vezes em CI. Em um trabalho não interativo, read pode atingir EOF imediatamente. Um erro claro é muito melhor do que um comando de implantação sendo executado com uma tag em branco.
Use timeouts para prompts que não devem bloquear para sempre
Um script de manutenção que espera por confirmação pode segurar silenciosamente uma implantação ou um job cron. read -t define um tempo limite em segundos:
if IFS= read -r -t 15 -p 'Restart service now? [y/N] ' answer; then
case $answer in
y|Y|yes|YES) systemctl restart myapp ;;
*) printf 'Skipped restart.\n' ;;
esac
else
printf '\nNo answer after 15 seconds; skipped restart.\n' >&2
fi
O suporte a timeout é um recurso do Bash, não um recurso do POSIX sh. Isso geralmente é bom para um artigo sobre Bash, mas vale a pena lembrar se um script pode ser executado com /bin/sh em uma imagem base pequena.
Oculte senhas, mas não finja que elas estão protegidas para sempre
read -s impede que os caracteres digitados sejam ecoados no terminal:
IFS= read -r -s -p 'Password: ' password
printf '\n'
IFS= read -r -s -p 'Confirm password: ' confirm_password
printf '\n'
if [[ $password != "$confirm_password" ]]; then
printf 'Passwords do not match.\n' >&2
exit 1
fi
Isso protege contra olhares indiscretos e rolagem de terminal. Não transforma o Bash em um gerenciador de segredos seguro. O valor ainda existe em uma variável do shell enquanto o script é executado. Não o imprima com set -x ativado, não o passe por linhas de comando que aparecem em listagens de processos e não o escreva em arquivos temporários. Se o segredo for para um fluxo de trabalho de produção sério, prefira um cofre de segredos, um arquivo de token com permissões estritas ou o prompt de senha nativo da ferramenta de destino.
Uma regra prática: desative o xtrace ao redor do manuseio de segredos se o script ao redor usar rastreamento.
set +x
IFS= read -r -s -p 'API token: ' api_token
printf '\n'
set -x
Ainda melhor, evite reativar o xtrace até que o token não seja mais referenciado por comandos.
Valide por lista de permissões, não por escapamento desejoso
A validação de entrada deve corresponder ao trabalho. Um nome de branch, um nome de usuário, um número de porta e uma descrição de forma livre são tipos diferentes de texto. Não sanitize tudo com uma função vaga.
Para um ambiente de implantação simples, permita apenas valores conhecidos:
IFS= read -r -p 'Environment [dev/staging/prod]: ' env_name
case $env_name in
dev|staging|prod) ;;
*)
printf 'Invalid environment: %s\n' "$env_name" >&2
exit 1
;;
esac
Para uma porta TCP, verifique a forma e o intervalo:
IFS= read -r -p 'Port: ' port
if ! [[ $port =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then
printf 'Enter a port from 1 to 65535.\n' >&2
exit 1
fi
Para um nome de arquivo local, decida o que você realmente permite. Se o seu script suporta apenas um nome de arquivo simples no diretório atual, diga isso e rejeite barras:
IFS= read -r -p 'Output filename: ' filename
if ! [[ $filename =~ ^[A-Za-z0-9._-]+$ ]]; then
printf 'Use only letters, numbers, dot, underscore, and dash.\n' >&2
exit 1
fi
printf 'Writing to %s\n' "$filename"
Evite o padrão de construir uma string de comando e depois executá-la com eval. printf %q pode exibir uma representação escapada do shell, mas não é uma licença para montar comandos não confiáveis. Prefira arrays para que o shell mantenha cada argumento separado:
cmd=(tar -czf "$filename.tar.gz" "$filename")
"${cmd[@]}"
Leia vários valores apenas quando a divisão for intencional
read first last divide em IFS. Se o usuário inserir mais palavras do que variáveis, a última variável recebe o resto. Isso pode ser útil para nomes, mas também pode surpreendê-lo.
IFS= read -r -p 'First and last name: ' first_name last_name
Se a entrada for Mary Jane Watson, first_name se torna Mary e last_name se torna Jane Watson. Se você precisar de toda a linha, leia em uma variável. Se você precisar de entrada estruturada, escolha um delimitador e analise-o deliberadamente.
Para valores separados por dois pontos:
IFS=: read -r host port <<<"$target"
Em seguida, valide ambos os campos. Não presuma que o delimitador apareceu.
Lide com padrões sem esconder erros
Os padrões são úteis quando são visíveis:
IFS= read -r -p 'Log level [INFO]: ' log_level
log_level=${log_level:-INFO}
Para operações destrutivas, evite padrões que fazem a coisa perigosa. Um prompt como Delete data? [y/N] deve tratar Enter como não, não sim.
IFS= read -r -p 'Delete local cache? [y/N] ' answer
case $answer in
y|Y|yes|YES) rm -rf -- "$cache_dir" ;;
*) printf 'Cache left in place.\n' ;;
esac
Observe o -- antes do caminho. Isso evita que um nome de arquivo começando com - seja interpretado como uma opção pelo rm.
Faça prompts funcionarem em pipelines e scripts
Se o seu script lê dados da entrada padrão, um prompt interativo pode acidentalmente consumir os dados canalizados em vez de ler do terminal. Nesse caso, leia os prompts de /dev/tty:
printf 'Continue? [y/N] ' > /dev/tty
IFS= read -r answer < /dev/tty
Este padrão é útil para ferramentas como:
generate-list | ./review-and-delete.sh
O script pode processar registros canalizados do stdin enquanto ainda pergunta ao operador por confirmação no terminal de controle.
Uma pequena função de prompt reutilizável
Para scripts com vários prompts, um pequeno auxiliar mantém o comportamento consistente:
prompt_required() {
local label=$1 value
while true; do
IFS= read -r -p "$label: " value || return 1
if [[ -n $value ]]; then
printf '%s\n' "$value"
return 0
fi
printf '%s is required.\n' "$label" >&2
done
}
project_name=$(prompt_required 'Project name') || exit 1
A função imprime o valor aceito no stdout, para que os chamadores possam capturá-lo. Erros vão para o stderr. Isso a mantém utilizável na substituição de comando sem misturar prompts e resultados.
A versão curta: read é seguro o suficiente quando você mantém o texto como dados. Use IFS= read -r, verifique falhas, oculte segredos com expectativas realistas, valide exatamente o que você planeja fazer e passe valores como argumentos entre aspas ou elementos de array. A maioria dos bugs do Bash relacionados a entrada desaparece quando esses hábitos se tornam automáticos.
Evite prompts sim/não que aceitam demais
Um prompt de confirmação deve ser chato e rigoroso. Não trate qualquer resposta não vazia como aprovação. Já vi scripts usarem este padrão:
read -r -p 'Continue? ' answer
if [[ $answer ]]; then
deploy_to_production
fi
Isso significa que no, wait e what does this do? contam como sim. Use uma declaração case e torne o padrão seguro:
IFS= read -r -p 'Deploy to production? Type yes to continue: ' answer
case $answer in
yes) deploy_to_production ;;
*)
printf 'Deployment cancelled.\n' >&2
exit 1
;;
esac
Para operações especialmente arriscadas, exigir o nome exato do recurso é melhor do que um prompt sim/não:
printf 'Type %s to delete this namespace: ' "$namespace"
IFS= read -r confirmation
if [[ $confirmation != "$namespace" ]]; then
printf 'Name did not match. Nothing deleted.\n' >&2
exit 1
fi
Isso protege contra alguém pressionando Enter em um prompt que não leu.
Cuidado com opções apenas de terminal
Algumas opções de read assumem um terminal. Entrada silenciosa, prompts e timeouts são projetados para uso interativo. Se o seu script pode ser executado em CI, um entrypoint Docker ou cron, verifique se o stdin é um terminal:
if [[ -t 0 ]]; then
IFS= read -r -p 'Release name: ' release_name
else
release_name=${RELEASE_NAME:?RELEASE_NAME is required in non-interactive mode}
fi
Isso dá aos humanos um prompt e à automação um contrato de variável de ambiente claro. Também evita que um job de build trave até que a plataforma o mate.
Não use read para formatos estruturados quando um analisador existe
Tudo bem ler um valor simples de uma pessoa. É menos bom analisar JSON, YAML, CSV ou sintaxe de shell com um loop read casual, a menos que o formato seja genuinamente simples. Uma vírgula dentro de um campo CSV ou uma aspa dentro de JSON pode quebrar rapidamente a análise escrita à mão.
Para JSON, use jq. Para arquivos .env, prefira um formato deliberadamente pequeno e documente-o. Se você ler configuração baseada em linha, preserve a linha e pule comentários explicitamente:
while IFS= read -r line; do
[[ -z $line || $line == \#* ]] && continue
printf 'config line: %s\n' "$line"
done < settings.conf
Esse loop não analisa magicamente todos os formatos de configuração. Ele apenas lê as linhas fielmente, que é o ponto de partida certo.
Uma revisão do mundo real antes de enviar
Antes de considerar um script ou configuração de contêiner finalizado, leia-o uma vez como se você fosse a próxima pessoa que terá que depurá-lo às 2 da manhã. Isso muda o que você percebe. Um prompt que fazia sentido enquanto escrevia o script pode ser ambíguo quando aparece em um log de CI. Um nome de serviço Docker que parecia óbvio pode não corresponder ao nome da variável no aplicativo. Um padrão Bash pode ser seguro para desenvolvimento e perigoso para produção.
Gosto de fazer um teste rápido com valores deliberadamente estranhos. Use um caminho com espaços. Use um valor opcional vazio. Tente um nome de arquivo que comece com um traço. Execute o script de um diretório de trabalho diferente. Inicie o contêiner sem uma variável de ambiente esperada. Esses testes não são sofisticados, mas pegam as suposições que geralmente quebram primeiro.
Verifique também a mensagem de falha. Se a única saída for failed, o conselho do artigo não chegou à implementação. Uma falha útil diz qual valor foi usado, qual verificação falhou e o que o operador pode mudar. Isso não significa despejar todas as variáveis de ambiente ou imprimir segredos. Significa ser específico onde a especificidade ajuda: o caminho de configuração, o nome do comando ausente, o nome da rede, o nome do host do serviço ou a porta que o processo tentou vincular.
O hábito final é manter os exemplos próximos da maneira como o sistema é realmente executado. Se a produção usa Compose, teste com Compose. Se um script é iniciado pelo systemd, teste-o com systemd ou com um ambiente igualmente mínimo. Se um comando deve ser seguro para copiar e colar, inclua as aspas, os separadores -- e a validação no próprio exemplo. Os leitores copiam padrões de trabalho com mais frequência do que copiam avisos.
Essa revisão não é burocracia. É como a pequena automação permanece chata. Chato é o que você quer de prompts de shell, carregadores de configuração, expansão de variáveis, diagnósticos de contêiner e rede Docker. Quanto menos surpreendente for o comportamento, mais fácil será para o próximo operador confiar nele.