Em shell script, funções podem retornar valores de duas formas principais: através do exit status (código de retorno) e através da saída padrão (stdout). Compreender a diferença entre esses mecanismos é fundamental para projetar scripts modulares e reutilizáveis. Nesta aula, exploraremos como capturar a saída de funções, usar variáveis globais para compartilhar dados e aplicar padrões de projeto para retornar valores complexos.

Vamos começar com uma distinção clara: o exit status é um número inteiro entre 0 e 255 que indica sucesso ou falha, enquanto a saída é qualquer texto escrito para stdout que pode ser capturado pelo chamador. Muitas vezes, precisamos retornar dados não numéricos ou múltiplos valores, o que exige técnicas como captura de saída ou variáveis globais.

Exit status vs saída

O exit status é o mecanismo nativo para indicar o resultado de uma função. Por padrão, uma função retorna o exit status do último comando executado. Podemos usar o comando return para especificar um valor numérico (0 a 255). O chamador pode verificar esse valor usando a variável $? imediatamente após a chamada da função.

Já a saída (stdout) é o que a função escreve na tela (ou em qualquer fluxo de saída). Para retornar dados textuais ou estruturados, a função pode ecoar strings, e o chamador pode capturar essa saída usando substituição de comando $(funcao) ou `funcao`. É importante notar que return e saída são independentes: uma função pode escrever na saída e ainda retornar um exit status diferente.

#!/bin/bash

# Função que retorna exit status
is_even() {
    local num=$1
    if (( num % 2 == 0 )); then
        return 0  # sucesso
    else
        return 1  # falha
    fi
}

# Função que retorna saída
get_greeting() {
    local name=$1
    echo "Olá, $name!"
}

# Chamando as funções
is_even 4
echo "Exit status: $?"  # 0

greeting=$(get_greeting "Maria")
echo "Saída capturada: $greeting"

No exemplo, is_even retorna 0 ou 1, que pode ser usado em condicionais. get_greeting escreve na saída, que é capturada pela variável greeting.

Capturando saída

Para capturar a saída de uma função, usamos a substituição de comando: $(funcao argumentos). Isso permite armazenar o resultado em uma variável ou usá-lo diretamente em outros comandos. É uma técnica poderosa, mas deve-se tomar cuidado para não incluir saídas indesejadas (como mensagens de depuração) no fluxo de saída.

#!/bin/bash

# Função que retorna um valor calculado
calculate_sum() {
    local a=$1 b=$2
    echo $((a + b))
}

# Capturando a saída
result=$(calculate_sum 5 7)
echo "A soma é: $result"

# Usando diretamente em uma expressão
if (( $(calculate_sum 2 3) > 4 )); then
    echo "Maior que 4"
fi

Para capturar múltiplas linhas, a variável conterá todas as linhas. Podemos usar IFS ou mapfile para processar linhas individuais. Cuidado com espaços em branco e newlines: a substituição de comando remove newlines finais, mas preserva internos.

#!/bin/bash

get_users() {
    echo "alice"
    echo "bob"
    echo "charlie"
}

# Captura múltiplas linhas
users=$(get_users)
echo "Usuários:"
echo "$users"

# Transformar em array
IFS=$'\n' read -r -d '' -a user_array < <(get_users && printf '\0')
echo "Primeiro usuário: ${user_array[0]}"

Variáveis globais

Outra forma de retornar valores é modificar variáveis globais. Isso é simples, mas pode tornar o script menos modular e mais propenso a efeitos colaterais. Em scripts pequenos, pode ser aceitável, mas em projetos maiores é preferível usar captura de saída ou outras técnicas.

Para evitar poluição do escopo global, use o prefixo g_ ou similar, e documente claramente quais variáveis são modificadas pela função. Lembre-se de que variáveis declaradas com local dentro da função não afetam o escopo global.

#!/bin/bash

# Variável global para armazenar resultado
g_result=""

# Função que modifica variável global
set_result() {
    g_result="Valor definido pela função"
}

set_result
echo "Resultado global: $g_result"

# Função que usa variável global como entrada/saída
g_counter=0
increment_counter() {
    local increment=$1
    g_counter=$((g_counter + increment))
}

increment_counter 5
echo "Contador: $g_counter"

Variáveis globais são úteis quando a função precisa retornar múltiplos valores ou quando o custo de capturar saída é proibitivo. No entanto, evite abusar: prefira funções que retornam via stdout e, para múltiplos valores, considere imprimir linhas separadas e parseá-las.

Padrões

Existem padrões comuns para retornar valores complexos em shell script. Um deles é usar um delimitador para separar múltiplos valores na saída. Outro é imprimir pares chave=valor, que podem ser avaliados com eval (com cuidado). Também é comum usar arrays associativos (Bash 4+) ou até mesmo JSON com ferramentas como jq.

Outro padrão é a função retornar um código de saída e, se necessário, escrever dados em um arquivo temporário ou em uma variável global. Para scripts mais robustos, considere o uso de named pipes ou descritores de arquivo.

#!/bin/bash

# Padrão: saída com delimitador
get_user_info() {
    local username=$1
    # Simula busca
    echo "$username:1000:1000:User,,,:/home/$username:/bin/bash"
}

info=$(get_user_info "alice")
IFS=':' read -r user uid gid desc home shell <<< "$info"
echo "Usuário: $user, UID: $uid, Shell: $shell"

# Padrão: pares chave=valor
get_config() {
    echo "nome=servidor"
    echo "porta=8080"
    echo "debug=true"
}

eval "$(get_config)"
echo "Nome: $nome, Porta: $porta, Debug: $debug"

# Padrão: array associativo (Bash 4+)
declare -A user_data
get_user_data() {
    local username=$1
    # Simula dados
    user_data[nome]="Alice"
    user_data[email]="alice@example.com"
}

get_user_data "alice"
echo "Email: ${user_data[email]}"

Cada padrão tem suas vantagens e desvantagens. A escolha depende da complexidade dos dados, da legibilidade e da manutenibilidade do script. Em geral, prefira a captura de saída com formatos simples e evite eval se possível, pois pode introduzir riscos de segurança.

Boas práticas

1. Use return apenas para indicar sucesso/falha (0 para sucesso, não-zero para erro).
2. Para retornar dados, use echo e capture com $().
3. Evite variáveis globais sempre que possível; prefira funções puras.
4. Documente o que a função retorna: se é exit status, saída, ou modifica globais.
5. Em funções que produzem saída, evite escrever mensagens de depuração em stdout; use stderr com >&2.

Referências

Exercícios

  1. Crie uma função chamada max que recebe dois números e retorna o maior deles (como saída). Capture o resultado e exiba.

    ✓ Resposta:
    #!/bin/bash
    
    max() {
        local a=$1 b=$2
        if (( a > b )); then
            echo "$a"
        else
            echo "$b"
        fi
    }
    
    result=$(max 10 20)
    echo "O maior é: $result"
    
  2. Escreva uma função is_file_empty que retorna 0 se o arquivo estiver vazio, 1 caso contrário. Use exit status.

    ✓ Resposta:
    #!/bin/bash
    
    is_file_empty() {
        local file=$1
        if [[ ! -s "$file" ]]; then
            return 0
        else
            return 1
        fi
    }
    
    if is_file_empty "/etc/passwd"; then
        echo "Arquivo vazio"
    else
        echo "Arquivo não vazio"
    fi
    
  3. Implemente uma função get_system_info que retorna o nome do host e a quantidade de memória total (em MB) em duas linhas separadas. Capture e exiba.

    ✓ Resposta:
    #!/bin/bash
    
    get_system_info() {
        echo "$(hostname)"
        echo "$(free -m | awk '/^Mem:/ {print $2}')"
    }
    
    mapfile -t info < <(get_system_info)
    echo "Host: ${info[0]}"
    echo "Memória total: ${info[1]} MB"
    
  4. Crie uma função que modifica uma variável global g_count incrementando-a em 1 a cada chamada. Faça três chamadas e exiba o valor final.

    ✓ Resposta:
    #!/bin/bash
    
    g_count=0
    
    increment() {
        g_count=$((g_count + 1))
    }
    
    increment
    increment
    increment
    echo "Valor final: $g_count"
    
  5. Escreva uma função parse_url que recebe uma URL e retorna o protocolo, domínio e caminho separados por dois-pontos (exemplo: https:example.com:/path). Capture e exiba cada parte.

    ✓ Resposta:
    #!/bin/bash
    
    parse_url() {
        local url=$1
        # Extrai protocolo
        local proto="${url%%://*}"
        local rest="${url#*://}"
        local domain="${rest%%/*}"
        local path="/${rest#*/}"
        echo "$proto:$domain:$path"
    }
    
    result=$(parse_url "https://example.com/path/to/page")
    IFS=':' read -r protocol domain path <<< "$result"
    echo "Protocolo: $protocol"
    echo "Domínio: $domain"
    echo "Caminho: $path"