Interactive scripts: read, select e menus

1. Fundamentos do comando read

O comando read é a porta de entrada para scripts interativos em Bash. Sua sintaxe básica é simples, mas as opções disponíveis oferecem grande flexibilidade:

read -p "Digite seu nome: " nome
echo "Olá, $nome!"

Opções essenciais:

  • -p "prompt" — Exibe um prompt antes da leitura
  • -s — Modo silencioso (útil para senhas)
  • -t N — Timeout de N segundos
  • -n N — Lê apenas N caracteres
# Exemplo com múltiplas opções
read -s -p "Senha: " senha
echo
read -t 5 -p "Nome (5 segundos): " nome || nome="Convidado"

Lendo múltiplas variáveis com IFS:

# Lendo dados separados por dois-pontos
IFS=':' read -p "Digite hora:minuto:segundo: " h m s
echo "São $h horas, $m minutos e $s segundos"

Tratamento de entrada vazia:

read -p "Nome do projeto [projeto_padrao]: " projeto
projeto=${projeto:-projeto_padrao}
echo "Nome definido: $projeto"

2. Validação e sanitização de entrada

Validar entradas é crucial para scripts robustos. Usamos expressões regulares com [[ ]] e loops while:

# Validando número inteiro
while true; do
    read -p "Digite um número inteiro: " numero
    if [[ "$numero" =~ ^-?[0-9]+$ ]]; then
        echo "Número válido: $numero"
        break
    else
        echo "Erro: digite apenas números inteiros."
    fi
done

Verificando múltiplos tipos:

# Validação de email simples
while true; do
    read -p "Email: " email
    if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
        echo "Email válido!"
        break
    fi
    echo "Formato inválido. Tente novamente."
done

Sempre use read -r para preservar barras invertidas e caracteres especiais:

read -r -p "Caminho do arquivo (use barras): " caminho
echo "Processando: $caminho"

3. Construindo menus com select

O comando select cria menus interativos automaticamente:

echo "Escolha uma opção:"
select opcao in "Listar" "Criar" "Deletar" "Sair"; do
    case $REPLY in
        1) echo "Listando arquivos..." ;;
        2) echo "Criando arquivo..." ;;
        3) echo "Deletando arquivo..." ;;
        4) echo "Saindo..."; break ;;
        *) echo "Opção inválida" ;;
    esac
done

Personalizando o prompt:

PS3="Digite sua escolha (1-4): "
select opcao in "Opção A" "Opção B" "Opção C" "Sair"; do
    # $REPLY contém o número digitado
    # $opcao contém o texto da opção
    echo "Você escolheu: $opcao (índice $REPLY)"
    [[ "$opcao" == "Sair" ]] && break
done

4. Menus dinâmicos e arrays associativos

Populando menus a partir de arrays:

opcoes=("Criar usuário" "Listar usuários" "Deletar usuário" "Sair")
PS3="Selecione: "
select acao in "${opcoes[@]}"; do
    case $REPLY in
        1) echo "Criando..." ;;
        2) echo "Listando..." ;;
        3) echo "Deletando..." ;;
        4) break ;;
    esac
done

Usando arrays associativos para mapear descrições a ações:

declare -A acoes
acoes["1: Criar"]="criar_usuario"
acoes["2: Listar"]="listar_usuarios"
acoes["3: Sair"]="sair"

select escolha in "${!acoes[@]}"; do
    funcao="${acoes[$escolha]}"
    if [[ -n "$funcao" ]]; then
        $funcao
    else
        echo "Opção inválida"
    fi
done

Menu de seleção de arquivos:

select arquivo in *; do
    if [[ -n "$arquivo" && -f "$arquivo" ]]; then
        echo "Arquivo selecionado: $arquivo"
        wc -l "$arquivo"
        break
    fi
    echo "Selecione um arquivo válido"
done

5. Navegação avançada com case e submenus

Combinando select com case para ações condicionais:

menu_principal() {
    PS3="Menu principal > "
    select opcao in "Gerenciar usuários" "Gerenciar arquivos" "Sair"; do
        case $REPLY in
            1) menu_usuarios ;;
            2) menu_arquivos ;;
            3) echo "Saindo..."; exit 0 ;;
            *) echo "Opção inválida" ;;
        esac
    done
}

menu_usuarios() {
    PS3="Usuários > "
    select opcao in "Criar" "Listar" "Voltar"; do
        case $REPLY in
            1) echo "Criando usuário..." ;;
            2) echo "Listando usuários..." ;;
            3) return ;;
            *) echo "Opção inválida" ;;
        esac
    done
}

Gerenciamento de estado entre menus:

declare -g usuario_atual=""
declare -g diretorio_atual="$PWD"

menu_arquivos() {
    PS3="Arquivos em $diretorio_atual > "
    select arquivo in * "Voltar"; do
        if [[ "$arquivo" == "Voltar" ]]; then
            return
        elif [[ -f "$arquivo" ]]; then
            echo "Processando $arquivo..."
        fi
    done
}

6. Tratamento de interrupções e sinais

Capturando Ctrl+C para saída limpa:

cleanup() {
    echo -e "\n\nOperação interrompida pelo usuário."
    stty sane
    exit 1
}

trap cleanup SIGINT SIGTERM

# Menu com timeout
while true; do
    if ! read -t 10 -p "Ação (ou timeout em 10s): " acao; then
        echo -e "\nTimeout atingido. Executando ação padrão..."
        acao="default"
    fi

    case $acao in
        "exit") break ;;
        "default") echo "Ação padrão executada" ;;
        *) echo "Comando: $acao" ;;
    esac
done

7. Integração com logging colorido e progresso

Adicionando cores a prompts e mensagens:

# Cores
VERDE='\033[0;32m'
VERMELHO='\033[0;31m'
AMARELO='\033[1;33m'
RESET='\033[0m'

log_info() { echo -e "${VERDE}[INFO]${RESET} $1"; }
log_erro() { echo -e "${VERMELHO}[ERRO]${RESET} $1" >&2; }
log_alerta() { echo -e "${AMARELO}[ALERTA]${RESET} $1"; }

# Barra de progresso simples
progresso() {
    local total=$1
    local atual=$2
    local percent=$(( atual * 100 / total ))
    local barras=$(( percent / 2 ))
    printf "\r[%-${barras}s%*s] %d%%" "#" "$((50-barras))" "" "$percent"
}

Registro de interações em log:

LOG_FILE="interacoes.log"

registrar_log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}

# Exemplo de uso no menu
select opcao in "Executar" "Sair"; do
    registrar_log "Usuário selecionou: $opcao"
    case $REPLY in
        1) 
            echo "Executando tarefa longa..."
            for i in {1..10}; do
                sleep 0.5
                progresso 10 $i
            done
            echo -e "\nTarefa concluída!"
            ;;
        2) break ;;
    esac
done

8. Boas práticas e exemplos completos

Estrutura modular com funções:

#!/bin/bash

# Configurações
LOG_FILE="gerenciador.log"
declare -a tarefas=()

# Funções de utilidade
log() { echo "[$(date '+%H:%M:%S')] $*" >> "$LOG_FILE"; }
erro() { echo "ERRO: $*" >&2; log "ERRO: $*"; }

# Funções do menu
adicionar_tarefa() {
    read -p "Descrição da tarefa: " descricao
    if [[ -z "$descricao" ]]; then
        erro "Descrição não pode ser vazia"
        return 1
    fi
    tarefas+=("$descricao")
    log "Tarefa adicionada: $descricao"
    echo "Tarefa adicionada com sucesso!"
}

listar_tarefas() {
    if [[ ${#tarefas[@]} -eq 0 ]]; then
        echo "Nenhuma tarefa cadastrada."
        return
    fi
    echo "=== TAREFAS ==="
    for i in "${!tarefas[@]}"; do
        echo "$((i+1)). ${tarefas[$i]}"
    done
}

remover_tarefa() {
    listar_tarefas
    read -p "Número da tarefa a remover: " num
    if [[ "$num" =~ ^[0-9]+$ ]] && (( num >= 1 && num <= ${#tarefas[@]} )); then
        echo "Removendo: ${tarefas[$((num-1))]}"
        unset 'tarefas[$((num-1))]'
        tarefas=("${tarefas[@]}")  # Reindexar array
        log "Tarefa $num removida"
    else
        erro "Número inválido"
    fi
}

# Menu principal
main() {
    echo "=== GERENCIADOR DE TAREFAS ==="
    PS3="Escolha uma opção: "

    while true; do
        select opcao in "Adicionar tarefa" "Listar tarefas" "Remover tarefa" "Sair"; do
            case $REPLY in
                1) adicionar_tarefa ;;
                2) listar_tarefas ;;
                3) remover_tarefa ;;
                4) 
                    echo "Saindo... Tarefas salvas em $LOG_FILE"
                    log "Usuário encerrou o programa"
                    exit 0
                    ;;
                *) echo "Opção inválida. Tente novamente." ;;
            esac
            break
        done
        echo
    done
}

# Tratamento de interrupção
trap 'echo -e "\n\nPrograma interrompido."; exit 1' SIGINT SIGTERM

# Iniciar programa
main "$@"

Validação de argumentos de linha de comando:

# Uso: ./script.sh [-v] [-l arquivo_log]
while getopts "vl:" opt; do
    case $opt in
        v) set -x ;;  # Modo verbose
        l) LOG_FILE="$OPTARG" ;;
        *) echo "Uso: $0 [-v] [-l arquivo_log]"; exit 1 ;;
    esac
done

Referências