Parsing de YAML e TOML no Bash

1. Introdução ao problema: por que parsear YAML e TOML no Bash?

O Bash, apesar de poderoso para automação de tarefas no terminal, possui limitações significativas quando o assunto é lidar com formatos de dados estruturados. Diferente de linguagens como Python ou JavaScript, o Bash não oferece mapas associativos complexos, listas aninhadas ou estruturas de dados tipadas. Arrays no Bash são unidimensionais e não suportam chaves arbitrárias de forma nativa (embora o declare -A ofereça arrays associativos a partir do Bash 4).

No entanto, cenários reais frequentemente exigem a leitura de arquivos de configuração em YAML (Docker Compose, Kubernetes, Ansible) ou TOML (pyproject.toml, Cargo.toml). Surge então a necessidade de parsear esses formatos diretamente de scripts Bash, seja para automatizar deploys, extrair metadados ou integrar ferramentas.

Existem duas abordagens principais: utilizar ferramentas externas especializadas (como yq e tomlq) ou implementar parsers caseiros com grep, sed e awk. Cada abordagem tem seus prós e contras, que exploraremos a seguir.

2. Parsing de YAML com ferramentas externas

A ferramenta mais robusta para lidar com YAML no Bash é o yq, um port do jq (JSON) para YAML. Sua sintaxe é familiar para quem já usa jq.

# Instalação via snap ou brew
sudo snap install yq
# ou
brew install yq

# Exemplo: extrair o nome de um serviço de um docker-compose.yml
yq e '.services.web.image' docker-compose.yml

Outra abordagem é converter YAML para JSON usando yaml2json e depois processar com jq:

# Convertendo YAML para JSON
yaml2json config.yml | jq '.database.host'

Exemplo prático com um docker-compose.yml:

# docker-compose.yml
version: '3'
services:
  web:
    image: nginx:latest
    ports:
      - "80:80"
  db:
    image: postgres:13
    environment:
      POSTGRES_PASSWORD: secret

# Extraindo todas as imagens
yq e '.services.*.image' docker-compose.yml
# Saída: nginx:latest
#        postgres:13

3. Parsing de YAML sem dependências externas (abordagens caseiras)

Para YAML simples (apenas chave-valor, sem aninhamento profundo), é possível usar grep, sed e awk:

# YAML simples: config.yaml
app_name: meu_app
version: 1.0
debug: true

# Script para parsear
parse_simple_yaml() {
    local prefix=$2
    local s='[[:space:]]*'
    local w='[a-zA-Z0-9_]*'
    local fs=$(echo @|tr @ '\034')
    sed -ne "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \
        -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" "$1" |
    awk -F$fs '{
        indent = length($1)/2;
        vname[indent] = $2;
        for (i in vname) {if (i > indent) {delete vname[i]}}
        if (length($3) > 0) {
            vn=""; for (i=0; i<indent; i++) {vn=(vn)(vname[i])("_")}
            printf("%s%s=\"%s\"\n", vn, $2, $3);
        }
    }'
}

eval $(parse_simple_yaml config.yaml)
echo $app_name  # meu_app

Limitações importantes: indentação irregular, listas, strings multilinha e comentários podem quebrar o parser. Para YAML complexo, sempre prefira yq.

4. Parsing de TOML com ferramentas externas

O tomlq (parte do ecossistema yq) é a ferramenta mais direta:

# Instalação
pip install tomlq  # ou via yq

# Extraindo dependências de pyproject.toml
tomlq e '.project.dependencies[]' pyproject.toml

O remarshal é outro conversor versátil que suporta múltiplos formatos:

# Instalação
pip install remarshal

# Convertendo TOML para JSON
remarshal -if toml -of json pyproject.toml | jq '.tool.poetry.dependencies'

Exemplo com Cargo.toml:

# Cargo.toml
[package]
name = "meu_projeto"
version = "0.1.0"

[dependencies]
serde = "1.0"
tokio = { version = "1", features = ["full"] }

# Extraindo nome do pacote
tomlq e '.package.name' Cargo.toml
# Saída: meu_projeto

5. Parsing de TOML com Bash puro (quando não há ferramentas)

Para situações onde não é possível instalar dependências, um parser TOML básico pode ser implementado:

parse_toml_simple() {
    local file="$1"
    local current_section=""
    local key value

    while IFS= read -r line; do
        # Ignorar comentários e linhas vazias
        [[ "$line" =~ ^[[:space:]]*# ]] && continue
        [[ -z "$line" ]] && continue

        # Detectar seção
        if [[ "$line" =~ ^\[([^\]]+)\]$ ]]; then
            current_section="${BASH_REMATCH[1]}"
            current_section="${current_section//./_}"
            continue
        fi

        # Parsear chave=valor
        if [[ "$line" =~ ^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_-]*)[[:space:]]*=[[:space:]]*\"(.*)\"[[:space:]]*$ ]]; then
            key="${BASH_REMATCH[1]}"
            value="${BASH_REMATCH[2]}"
            if [[ -n "$current_section" ]]; then
                printf "%s_%s=\"%s\"\n" "$current_section" "$key" "$value"
            else
                printf "%s=\"%s\"\n" "$key" "$value"
            fi
        elif [[ "$line" =~ ^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_-]*)[[:space:]]*=[[:space:]]*(true|false|[0-9]+)\s*$ ]]; then
            key="${BASH_REMATCH[1]}"
            value="${BASH_REMATCH[2]}"
            if [[ -n "$current_section" ]]; then
                printf "%s_%s=%s\n" "$current_section" "$key" "$value"
            else
                printf "%s=%s\n" "$key" "$value"
            fi
        fi
    done < "$file"
}

eval $(parse_toml_simple config.toml)
echo $package_name  # valor extraído

Limitações: arrays inline, tabelas aninhadas [a.b.c] e datas não são suportados nessa implementação simplificada.

6. Técnicas avançadas e boas práticas

Uma técnica poderosa é combinar source com arquivos convertidos para variáveis de ambiente:

# Converter YAML para formato de variáveis e source
yq e '.. | select(tag == "!!str" or tag == "!!int") | path | join("_") + "=" + .' config.yml > /tmp/env_vars.sh
source /tmp/env_vars.sh

Tratamento de erros e fallback:

get_value() {
    local key="$1"
    local default="$2"
    local value

    value=$(yq e ".$key" config.yml 2>/dev/null)
    if [[ "$value" == "null" || -z "$value" ]]; then
        echo "$default"
    else
        echo "$value"
    fi
}

# Uso
DB_HOST=$(get_value "database.host" "localhost")

Performance: para arquivos pequenos (< 100KB), scripts puros podem ser mais rápidos. Para arquivos grandes ou estruturas complexas, ferramentas externas são significativamente mais eficientes.

7. Exemplo completo: script de deploy que lê configuração YAML e TOML

#!/bin/bash

# Script de deploy universal
DEPLOY_CONFIG="deploy.conf"

# Detecção automática do formato
detect_format() {
    local file="$1"
    case "$file" in
        *.yml|*.yaml) echo "yaml" ;;
        *.toml)       echo "toml" ;;
        *)            echo "unknown" ;;
    esac
}

# Função genérica de parse
parse_config() {
    local file="$1"
    local format=$(detect_format "$file")

    case "$format" in
        yaml)
            yq e "$2" "$file" 2>/dev/null || echo ""
            ;;
        toml)
            tomlq e "$2" "$file" 2>/dev/null || echo ""
            ;;
        *)
            echo "Formato não suportado: $file" >&2
            return 1
            ;;
    esac
}

# Extrair valores
APP_NAME=$(parse_config "$DEPLOY_CONFIG" '.app.name')
APP_VERSION=$(parse_config "$DEPLOY_CONFIG" '.app.version')
BUILD_CMD=$(parse_config "$DEPLOY_CONFIG" '.build.command')
DEPLOY_CMD=$(parse_config "$DEPLOY_CONFIG" '.deploy.command')

# Executar build
echo "Deployando $APP_NAME v$APP_VERSION"
eval "$BUILD_CMD" && eval "$DEPLOY_CMD"

8. Considerações finais e referências

Para YAML, o yq é a ferramenta mais madura e recomendada. Para TOML, tomlq ou remarshal oferecem boa cobertura. Scripts puros são viáveis apenas para casos muito simples e controlados. Em ambientes onde Python está disponível, usar python3 -c com bibliotecas padrão (yaml, tomllib) é uma alternativa robusta e portável.

A escolha entre ferramentas externas e scripts puros deve considerar: complexidade dos dados, necessidade de performance, portabilidade do ambiente e tolerância a falhas. Para scripts de produção, sempre prefira ferramentas especializadas.

Referências