Retornando valores de funções
Aula sobre como retornar valores de funções em shell script, abordando a diferença entre exit status e saída, captura de saída, uso de variáveis globais e padrões de projeto. Exemplos práticos e exercícios com respostas colapsáveis.
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
- GNU Bash Manual: Shell Functions
- Advanced Bash-Scripting Guide: Functions
- Bash FAQ: How can I return multiple values from a function?
- Stack Overflow: Return value in a bash function
- ShellCheck - Shell script analysis tool
Exercícios
Crie uma função chamada
maxque 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"Escreva uma função
is_file_emptyque 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" fiImplemente uma função
get_system_infoque 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"Crie uma função que modifica uma variável global
g_countincrementando-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"Escreva uma função
parse_urlque 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"