Progress indicators e logging colorido

1. Fundamentos de Logging Colorido no Terminal

Terminais modernos suportam códigos ANSI para colorir saídas de texto. Os códigos básicos são:

\e[31m  → vermelho
\e[32m  → verde
\e[33m  → amarelo
\e[34m  → azul
\e[0m   → reset (volta ao padrão)

Uma abordagem robusta é criar funções helper que verificam se a saída é um terminal real (-t). Isso evita enviar códigos ANSI para pipes ou arquivos:

#!/bin/bash

info() {
    if [[ -t 1 ]]; then
        echo -e "\e[34m[INFO]\e[0m $*"
    else
        echo "[INFO] $*"
    fi
}

success() {
    if [[ -t 1 ]]; then
        echo -e "\e[32m[OK]\e[0m $*"
    else
        echo "[OK] $*"
    fi
}

warn() {
    if [[ -t 1 ]]; then
        echo -e "\e[33m[WARN]\e[0m $*" >&2
    else
        echo "[WARN] $*" >&2
    fi
}

error() {
    if [[ -t 1 ]]; then
        echo -e "\e[31m[ERROR]\e[0m $*" >&2
    else
        echo "[ERROR] $*" >&2
    fi
}

Para maior portabilidade entre terminais, use tput em vez de códigos ANSI fixos:

RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
BLUE=$(tput setaf 4)
RESET=$(tput sgr0)

2. Estruturando Mensagens de Log com Níveis

Defina níveis de log com numeração crescente de gravidade: DEBUG (0), INFO (1), WARN (2), ERROR (3), FATAL (4). Cada nível recebe timestamp e prefixo colorido:

LOG_LEVEL=1  # 0=debug, 1=info, 2=warn, 3=error, 4=fatal

log() {
    local level=$1
    local message=$2
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')

    if (( level < LOG_LEVEL )); then
        return
    fi

    case $level in
        0) [[ -t 1 ]] && echo -e "\e[90m[$timestamp] [DEBUG]\e[0m $message" || echo "[$timestamp] [DEBUG] $message" ;;
        1) [[ -t 1 ]] && echo -e "\e[34m[$timestamp] [INFO]\e[0m $message" || echo "[$timestamp] [INFO] $message" ;;
        2) [[ -t 1 ]] && echo -e "\e[33m[$timestamp] [WARN]\e[0m $message" >&2 || echo "[$timestamp] [WARN] $message" >&2 ;;
        3) [[ -t 1 ]] && echo -e "\e[31m[$timestamp] [ERROR]\e[0m $message" >&2 || echo "[$timestamp] [ERROR] $message" >&2 ;;
        4) [[ -t 1 ]] && echo -e "\e[41m\e[97m[$timestamp] [FATAL]\e[0m $message" >&2 || echo "[$timestamp] [FATAL] $message" >&2 ;;
    esac
}

Redirecione corretamente: mensagens informativas vão para stdout (fd 1), enquanto avisos e erros vão para stderr (fd 2).

3. Logging para Arquivo vs. Terminal

Para duplicar a saída simultaneamente para terminal e arquivo, use tee com descritores de arquivo customizados:

exec 3>&1  # Salva stdout original
exec 4>&2  # Salva stderr original

# Redireciona tudo para tee
exec 1> >(tee -a /var/log/script.log)
exec 2> >(tee -a /var/log/script.log >&2)

# Agora toda saída vai para terminal E arquivo
echo "Esta mensagem aparece nos dois lugares"

Para logging condicional com cores apenas no terminal:

log_to_file() {
    local level=$1
    local message=$2
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[$timestamp] [$level] $message" >> /var/log/script.log
}

log_dual() {
    local level=$1
    local message=$2

    # No terminal (com cores)
    log "$level" "$message"

    # No arquivo (sem cores)
    log_to_file "$level" "$message"
}

4. Progress Bar Simples com printf e \r

O caractere \r (carriage return) permite sobrescrever a linha atual. Uma barra de progresso básica:

progress_bar() {
    local current=$1
    local total=$2
    local width=50
    local percent=$(( current * 100 / total ))
    local filled=$(( current * width / total ))
    local empty=$(( width - filled ))

    printf "\r["
    printf "%0.s#" $(seq 1 $filled)
    printf "%0.s-" $(seq 1 $empty)
    printf "] %d%%" $percent
}

# Uso:
for i in $(seq 1 100); do
    progress_bar $i 100
    sleep 0.05
done
echo

Para largura dinâmica baseada no terminal:

progress_bar_dynamic() {
    local current=$1
    local total=$2
    local width=$(( COLUMNS - 10 ))  # reserva espaço para [ ] e percentual
    local percent=$(( current * 100 / total ))
    local filled=$(( current * width / total ))
    local empty=$(( width - filled ))

    printf "\r["
    printf "%0.s#" $(seq 1 $filled)
    printf "%0.s-" $(seq 1 $empty)
    printf "] %3d%%" $percent
}

5. Spinners Animados com Caracteres Unicode

Spinners simples com caracteres ASCII e Unicode:

spinner() {
    local pid=$1
    local delay=0.1
    local spinstr='|/-\'
    local spinstr_unicode='⣾⣽⣻⢿⡿⣟⣯⣷'

    # Salva posição do cursor
    echo -ne "\e[s"

    while ps -p $pid > /dev/null 2>&1; do
        local temp=${spinstr_unicode#?}
        printf "\e[u %c " "$spinstr_unicode"
        local spinstr_unicode=$temp${spinstr_unicode%"$temp"}
        sleep $delay
    done

    # Restaura cursor e limpa
    printf "\e[u   \e[u"
}

# Uso:
long_running_task &
spinner $!
wait

6. Indicadores Avançados com pv e dialog

O comando pv (pipe viewer) fornece progresso automático:

# Progresso durante cópia de arquivo grande
pv arquivo_grande.iso > /dev/null

# Com taxa de transferência e tempo estimado
tar czf - /dados | pv -s $(du -sb /dados | awk '{print $1}') > backup.tar.gz

Para barras de progresso em modo texto interativo, use dialog:

(
    for i in $(seq 1 100); do
        echo $i
        sleep 0.05
    done
) | dialog --gauge "Processando..." 6 50 0

7. Tratamento de Interrupções e Redesenho

Capture SIGINT para limpar a tela antes de sair:

cleanup() {
    # Mostra cursor novamente
    echo -ne "\e[?25h"
    # Reseta cores
    echo -ne "\e[0m"
    # Limpa linha atual
    echo -ne "\r\e[K"
    exit 1
}

trap cleanup SIGINT SIGTERM

# Oculta cursor durante animações
echo -ne "\e[?25l"

# ... código com progresso ...

# Restaura cursor no final
echo -ne "\e[?25h"

8. Exemplo Prático: Script de Backup com Log e Progresso

#!/bin/bash
set -e

LOG_FILE="/var/log/backup.log"
BACKUP_DIR="/backup"
SOURCE_DIR="/dados"

# Configura cores
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
BLUE=$(tput setaf 4)
RESET=$(tput sgr0)

# Função de log combinada
log_progress() {
    local level=$1
    local message=$2
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')

    # Log para arquivo (sem cores)
    echo "[$timestamp] [$level] $message" >> "$LOG_FILE"

    # Log para terminal (com cores)
    case $level in
        INFO)  echo -e "${BLUE}[$timestamp] [INFO]${RESET} $message" ;;
        OK)    echo -e "${GREEN}[$timestamp] [OK]${RESET} $message" ;;
        WARN)  echo -e "${YELLOW}[$timestamp] [WARN]${RESET} $message" >&2 ;;
        ERROR) echo -e "${RED}[$timestamp] [ERROR]${RESET} $message" >&2 ;;
    esac
}

# Função de progresso
progress() {
    local current=$1
    local total=$2
    local width=40
    local percent=$(( current * 100 / total ))
    local filled=$(( current * width / total ))
    local empty=$(( width - filled ))

    printf "\r["
    printf "%0.s#" $(seq 1 $filled)
    printf "%0.s-" $(seq 1 $empty)
    printf "] %3d%%" $percent
}

# Tratamento de interrupção
cleanup() {
    echo -e "\n${YELLOW}Backup interrompido pelo usuário${RESET}"
    log_progress "WARN" "Backup interrompido"
    echo -ne "\e[?25h"
    exit 1
}

trap cleanup SIGINT SIGTERM

# Início do backup
log_progress "INFO" "Iniciando backup de $SOURCE_DIR para $BACKUP_DIR"
mkdir -p "$BACKUP_DIR"

# Oculta cursor
echo -ne "\e[?25l"

# Simula cópia de arquivos
total_files=50
for i in $(seq 1 $total_files); do
    progress $i $total_files
    sleep 0.1  # Simula processamento
done

echo
log_progress "OK" "Backup concluído com sucesso"

# Restaura cursor
echo -ne "\e[?25h"

Referências