Como Solucionar Problemas de Variáveis Não Definidas em Scripts Bash

ShellBeginner
Pratique Agora

Introdução

Variáveis não definidas (unbound) em scripts Bash podem levar a comportamentos inesperados e erros. Neste tutorial, exploraremos os fundamentos das variáveis de shell, aprenderemos a identificar quando as variáveis não estão definidas (não setadas) e implementaremos estratégias eficazes para lidar com elas. Ao final deste laboratório, você será capaz de escrever scripts Bash mais confiáveis que lidam com problemas de variáveis de forma elegante.

Criando Seu Primeiro Script com Variáveis de Shell

Variáveis de shell são locais de armazenamento nomeados que contêm valores em seus scripts Bash. Nesta etapa, criaremos um script simples para entender como as variáveis funcionam e ver como elas são usadas.

Entendendo as Variáveis de Shell

Variáveis em Bash permitem que você armazene dados que podem ser referenciados e manipulados em todo o seu script. Elas são essenciais para escrever scripts flexíveis e reutilizáveis.

Vamos começar criando um novo arquivo de script em nosso diretório de projeto:

  1. Abra o WebIDE e crie um novo arquivo chamado variables.sh no diretório /home/labex/project

  2. Adicione o seguinte conteúdo ao arquivo:

#!/bin/bash

## Declaring variables
name="LabEx User"
age=25
current_date=$(date)

## Accessing variables
echo "Hello, my name is $name"
echo "I am $age years old"
echo "Today is: $current_date"

## Variables with spaces need quotes
greeting="Welcome to Bash scripting"
echo "$greeting"
  1. Salve o arquivo

  2. Torne o script executável executando este comando no terminal:

chmod +x variables.sh
  1. Execute seu script:
./variables.sh

Você deve ver uma saída semelhante a esta:

Hello, my name is LabEx User
I am 25 years old
Today is: Mon Jan 1 12:34:56 UTC 2023
Welcome to Bash scripting

Regras de Nomenclatura de Variáveis

Ao nomear variáveis em Bash, tenha estas regras em mente:

  • Nomes de variáveis podem conter letras, números e underscores (sublinhados)
  • Nomes de variáveis não podem começar com um número
  • Nenhum espaço é permitido em torno do sinal de igual ao atribuir valores
  • Nomes de variáveis são case-sensitive (sensíveis a maiúsculas e minúsculas) (NAME e name são variáveis diferentes)

Vamos adicionar mais alguns exemplos ao nosso script. Abra variables.sh novamente e adicione o seguinte ao final:

## Examples of valid variable names
user_1="John"
HOME_DIR="/home/user"
count=42

## Printing the variables
echo "User: $user_1"
echo "Home directory: $HOME_DIR"
echo "Count: $count"

Salve o arquivo e execute-o novamente:

./variables.sh

A saída adicional deve ser semelhante a:

User: John
Home directory: /home/user
Count: 42

Agora você criou seu primeiro script Bash que usa variáveis corretamente!

Encontrando Variáveis Não Definidas

Agora que entendemos os conceitos básicos de variáveis, vamos explorar o que acontece quando tentamos usar uma variável que não recebeu um valor – uma variável não definida (unbound).

O que são Variáveis Não Definidas?

Uma variável não definida, também chamada de variável não setada (unset), é aquela que não recebeu um valor em seu script. Quando você tenta usar uma variável não definida, o Bash normalmente a trata como uma string vazia, o que pode causar bugs sutis em seus scripts.

Vamos criar um novo script para ver isso em ação:

  1. Crie um novo arquivo chamado unbound.sh em seu diretório de projeto com o seguinte conteúdo:
#!/bin/bash

## A variable that is properly set
username="labex"

## Try to use a variable that hasn't been set
echo "Hello, $username! Your home directory is $home_dir"

## Try to do math with an unbound variable
total=$((count + 5))
echo "Total: $total"
  1. Torne o script executável:
chmod +x unbound.sh
  1. Execute o script:
./unbound.sh

Você deve ver uma saída semelhante a esta:

Hello, labex! Your home directory is

Total: 5

Observe que $home_dir aparece como um espaço em branco, e $count é tratado como 0 no cálculo. Esse comportamento silencioso pode levar a bugs que são difíceis de encontrar.

Detectando Variáveis Não Definidas

Vamos modificar nosso script para ajudar a detectar essas variáveis não definidas. Uma maneira de fazer isso é usar verificações condicionais antes de usar as variáveis:

  1. Abra unbound.sh e substitua seu conteúdo por:
#!/bin/bash

## A variable that is properly set
username="labex"

## Check if home_dir is set before using it
if [ -z "$home_dir" ]; then
  echo "Warning: home_dir is not set!"
  home_dir="/home/default"
  echo "Using default: $home_dir"
else
  echo "Home directory is: $home_dir"
fi

## Check if count is set before doing math
if [ -z "$count" ]; then
  echo "Warning: count is not set!"
  count=10
  echo "Using default count: $count"
fi

total=$((count + 5))
echo "Total: $total"
  1. Salve o arquivo e execute-o novamente:
./unbound.sh

A saída agora deve ser:

Warning: home_dir is not set!
Using default: /home/default
Warning: count is not set!
Using default count: 10
Total: 15

Essa abordagem ajuda você a detectar variáveis não definidas e a tratá-las explicitamente, em vez de deixá-las causar erros silenciosos.

Testando com a Opção set -u

Outra maneira de detectar variáveis não definidas é usar a opção set -u, que faz com que o Bash saia imediatamente quando encontra uma variável não definida:

  1. Crie um novo arquivo chamado strict.sh com o seguinte conteúdo:
#!/bin/bash
set -u ## Exit when unbound variable is used

echo "My username is $username"
echo "My favorite color is $favorite_color"
echo "This line won't be reached if favorite_color isn't set"
  1. Torne-o executável:
chmod +x strict.sh
  1. Execute o script:
./strict.sh

Você deve ver uma mensagem de erro semelhante a:

./strict.sh: line 4: favorite_color: unbound variable

O script sai imediatamente na linha 4 quando tenta usar a variável não definida $favorite_color. Isso ajuda a detectar variáveis não definidas no início, evitando possíveis problemas mais tarde no script.

Lidando com Variáveis Não Definidas com Expansão de Parâmetros

Um dos recursos mais poderosos do Bash para lidar com variáveis não definidas é a expansão de parâmetros. Ela permite que você forneça valores padrão e execute várias operações em variáveis.

Usando Valores Padrão com Expansão de Parâmetros

A expansão de parâmetros oferece várias maneiras de lidar com variáveis não definidas de forma elegante. Vamos criar um novo script para demonstrar essas técnicas:

  1. Crie um novo arquivo chamado parameter_expansion.sh com o seguinte conteúdo:
#!/bin/bash

## Default value with ${parameter:-default}
## If parameter is unset or null, 'default' is used
echo "1. Username: ${username:-anonymous}"

## Define username and try again
username="labex"
echo "2. Username: ${username:-anonymous}"

## Default value with ${parameter-default}
## Only if parameter is unset, 'default' is used
unset username
empty_var=""
echo "3. Username (unset): ${username-anonymous}"
echo "4. Empty variable: ${empty_var-not empty}"

## Assign default with ${parameter:=default}
## If parameter is unset or null, set it to 'default'
echo "5. Language: ${language:=bash}"
echo "6. Language is now set to: $language"

## Display error with ${parameter:?message}
## If parameter is unset or null, show error message and exit
echo "7. About to check required parameter..."
## Uncomment the line below to see the error
## echo "Config file: ${config_file:?not specified}"

echo "8. Script continues..."
  1. Torne o script executável:
chmod +x parameter_expansion.sh
  1. Execute o script:
./parameter_expansion.sh

Você deve ver uma saída como:

1. Username: anonymous
2. Username: labex
3. Username (unset): anonymous
4. Empty variable:
5. Language: bash
6. Language is now set to: bash
7. About to check required parameter...
8. Script continues...

Padrões Comuns de Expansão de Parâmetros

Aqui está um resumo dos padrões de expansão de parâmetros que acabamos de usar:

Sintaxe Descrição
${var:-default} Usa o padrão se var não estiver definido ou for nulo
${var-default} Usa o padrão somente se var não estiver definido
${var:=default} Define var como padrão se não estiver definido ou for nulo
${var:?message} Exibe um erro se var não estiver definido ou for nulo

Criando um Exemplo Prático

Vamos criar um script mais prático que usa a expansão de parâmetros para lidar com configurações:

  1. Crie um novo arquivo chamado config_script.sh com o seguinte conteúdo:
#!/bin/bash

## Script that processes files with configurable settings

## Use parameter expansion to set defaults for all configuration options
BACKUP_DIR=${BACKUP_DIR:-"/tmp/backups"}
MAX_FILES=${MAX_FILES:-5}
FILE_TYPE=${FILE_TYPE:-"txt"}
VERBOSE=${VERBOSE:-"no"}

echo "Configuration settings:"
echo "----------------------"
echo "Backup directory: $BACKUP_DIR"
echo "Maximum files: $MAX_FILES"
echo "File type: $FILE_TYPE"
echo "Verbose mode: $VERBOSE"

## Create backup directory if it doesn't exist
if [ ! -d "$BACKUP_DIR" ]; then
  echo "Creating backup directory: $BACKUP_DIR"
  mkdir -p "$BACKUP_DIR"
fi

## Search for files of specified type (in current directory for demo)
echo "Searching for .$FILE_TYPE files..."
file_count=$(find . -maxdepth 1 -name "*.$FILE_TYPE" | wc -l)

echo "Found $file_count .$FILE_TYPE files"

## Enable verbose output if requested
if [ "$VERBOSE" = "yes" ]; then
  echo "Files found:"
  find . -maxdepth 1 -name "*.$FILE_TYPE" -type f
fi

echo "Script completed successfully!"
  1. Torne o script executável:
chmod +x config_script.sh
  1. Execute o script com valores padrão:
./config_script.sh
  1. Agora execute-o novamente com algumas configurações personalizadas:
FILE_TYPE="sh" VERBOSE="yes" ./config_script.sh

A saída deve mostrar seus scripts bash (arquivos .sh) no diretório atual.

Este script demonstra como usar a expansão de parâmetros para definir valores padrão para variáveis de configuração, tornando seu script flexível e fácil de usar.

Usando o Modo Estrito para Scripts Robustos

Para criar scripts Bash mais confiáveis e resistentes a erros, muitos desenvolvedores usam o que é frequentemente chamado de "modo estrito" (strict mode). Isso inclui um conjunto de opções do Bash que ajudam a detectar erros comuns, incluindo variáveis não definidas.

O que é Modo Estrito?

O modo estrito normalmente envolve a ativação dessas opções no início do seu script:

  • set -e: Sai imediatamente se um comando sair com um status diferente de zero
  • set -u: Sai se uma variável não definida for referenciada
  • set -o pipefail: Faz com que um pipeline falhe se qualquer comando nele falhar

Vamos ver como implementar isso em um script:

  1. Crie um novo arquivo chamado strict_mode.sh com o seguinte conteúdo:
#!/bin/bash
## Strict mode
set -euo pipefail

echo "Starting strict mode script..."

## Define some variables
username="labex"
project_dir="/home/labex/project"

## This function requires two parameters
process_data() {
  local input="$1"
  local output="${2:-output.txt}"

  echo "Processing $input and saving to $output"
  ## For demo purposes, just echo to the output file
  echo "Processed data from $input" > "$output"
}

## Call the function with required arguments
process_data "input.txt"

## Try to access a non-existent file
## Uncomment the next line to see how strict mode handles errors
## cat non_existent_file.txt

echo "Script completed successfully"
  1. Torne o script executável:
chmod +x strict_mode.sh
  1. Execute o script:
./strict_mode.sh

Você deve ver:

Starting strict mode script...
Processing input.txt and saving to output.txt
Script completed successfully
  1. Verifique se o arquivo de saída foi criado:
cat output.txt

Você deve ver:

Processed data from input.txt

Testando o Tratamento de Erros no Modo Estrito

Agora, vamos modificar nosso script para ver como o modo estrito lida com erros:

  1. Crie um novo arquivo chamado strict_mode_errors.sh com o seguinte conteúdo:
#!/bin/bash
## Strict mode
set -euo pipefail

echo "Starting script with error demonstrations..."

## Example 1: Unbound variable
echo "Example 1: About to use an unbound variable"
## Uncomment the next line to see the error
## echo "Home directory: $HOME_DIR"
echo "This line will not be reached if you uncomment the above line"

## Example 2: Command failure
echo "Example 2: About to run a failing command"
## Uncomment the next line to see the error
## grep "pattern" non_existent_file.txt
echo "This line will not be reached if you uncomment the above line"

## Example 3: Pipeline failure
echo "Example 3: About to create a failing pipeline"
## Uncomment the next line to see the error
## cat non_existent_file.txt | sort
echo "This line will not be reached if you uncomment the above line"

echo "Script completed without errors"
  1. Torne o script executável:
chmod +x strict_mode_errors.sh
  1. Execute o script:
./strict_mode_errors.sh
  1. Agora, edite o script para descomentar uma das linhas de erro (por exemplo, a linha com $HOME_DIR), salve-o e execute-o novamente:
./strict_mode_errors.sh

Você deve ver que o script sai imediatamente com uma mensagem de erro quando encontra a variável não definida:

Starting script with error demonstrations...
Example 1: About to use an unbound variable
./strict_mode_errors.sh: line 9: HOME_DIR: unbound variable

Isso demonstra como o modo estrito ajuda a identificar problemas no início do seu script.

Criando um Modelo de Script Robusto

Vamos criar um modelo para scripts Bash robustos que você pode usar para projetos futuros:

  1. Crie um novo arquivo chamado robust_script_template.sh com o seguinte conteúdo:
#!/bin/bash
## ====================================
## Robust Bash Script Template
## ====================================

## --- Strict mode ---
set -euo pipefail

## --- Script metadata ---
SCRIPT_NAME="$(basename "$0")"
SCRIPT_VERSION="1.0.0"

## --- Default configuration ---
DEBUG=${DEBUG:-false}
LOG_FILE=${LOG_FILE:-"/tmp/${SCRIPT_NAME}.log"}

## --- Helper functions ---
log() {
  local message="$1"
  local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
  echo "[$timestamp] $message" | tee -a "$LOG_FILE"
}

debug() {
  if [[ "$DEBUG" == "true" ]]; then
    log "DEBUG: $1"
  fi
}

error() {
  log "ERROR: $1"
  exit 1
}

usage() {
  cat << EOF
Usage: $SCRIPT_NAME [options]

A robust bash script template with error handling and logging.

Options:
  --help      Show this help message and exit
  --version   Show script version and exit

Environment variables:
  DEBUG       Set to 'true' to enable debug output
  LOG_FILE    Path to the log file (default: /tmp/${SCRIPT_NAME}.log)
EOF
}

## --- Process command line arguments ---
for arg in "$@"; do
  case $arg in
    --help)
      usage
      exit 0
      ;;
    --version)
      echo "$SCRIPT_NAME version $SCRIPT_VERSION"
      exit 0
      ;;
    *)
      ## Unknown option
      ;;
  esac
done

## --- Main script logic ---
main() {
  log "Starting $SCRIPT_NAME version $SCRIPT_VERSION"

  ## Your script logic goes here
  debug "Script executed with DEBUG=$DEBUG"
  log "Script executed by user: $(whoami)"
  log "Current directory: $(pwd)"

  ## Example of using a variable with default value
  TARGET_DIR=${TARGET_DIR:-$(pwd)}
  log "Target directory: $TARGET_DIR"

  log "$SCRIPT_NAME completed successfully"
}

## --- Run main function ---
main
  1. Torne o script executável:
chmod +x robust_script_template.sh
  1. Execute o script:
./robust_script_template.sh
  1. Tente executá-lo com a depuração ativada:
DEBUG=true ./robust_script_template.sh

Este modelo inclui:

  • Configurações do modo estrito para detectar erros
  • Configuração padrão com expansão de parâmetros
  • Funções de registro e depuração
  • Processamento de argumentos de linha de comando
  • Uma estrutura limpa com uma função principal

Você pode usar este modelo como ponto de partida para seus próprios scripts Bash para torná-los mais robustos e fáceis de manter.

Criando uma Aplicação do Mundo Real

Agora que aprendemos sobre variáveis, variáveis não definidas, expansão de parâmetros e modo estrito, vamos combinar todos esses conceitos em um script prático. Criaremos um utilitário simples de backup de arquivos que demonstra as melhores práticas para lidar com variáveis em Bash.

Planejando Nosso Script de Backup

Nosso script de backup irá:

  1. Receber um diretório de origem e um destino de backup como entradas
  2. Permitir a configuração por meio de variáveis de ambiente ou argumentos de linha de comando
  3. Lidar com erros de forma elegante usando o modo estrito
  4. Fornecer feedback útil ao usuário

Criando o Script de Backup

  1. Crie um novo arquivo chamado backup.sh com o seguinte conteúdo:
#!/bin/bash
## ====================================
## File Backup Utility
## ====================================

## --- Strict mode ---
set -euo pipefail

## --- Script metadata ---
SCRIPT_NAME="$(basename "$0")"
SCRIPT_VERSION="1.0.0"

## --- Default configuration ---
SOURCE_DIR=${SOURCE_DIR:-"$(pwd)"}
BACKUP_DIR=${BACKUP_DIR:-"/tmp/backups"}
BACKUP_NAME=${BACKUP_NAME:-"backup_$(date +%Y%m%d_%H%M%S)"}
EXCLUDE_PATTERN=${EXCLUDE_PATTERN:-"*.tmp"}
VERBOSE=${VERBOSE:-"false"}

## --- Helper functions ---
log() {
  if [[ "$VERBOSE" == "true" ]]; then
    echo "$(date "+%Y-%m-%d %H:%M:%S") - $1"
  fi
}

error() {
  echo "ERROR: $1" >&2
  exit 1
}

usage() {
  cat << EOF
Usage: $SCRIPT_NAME [options]

A simple file backup utility.

Options:
  -s, --source DIR      Source directory to backup (default: current directory)
  -d, --destination DIR Backup destination directory (default: /tmp/backups)
  -n, --name NAME       Name for the backup archive (default: backup_YYYYMMDD_HHMMSS)
  -e, --exclude PATTERN Files to exclude (default: *.tmp)
  -v, --verbose         Enable verbose output
  -h, --help            Show this help message and exit

Environment variables:
  SOURCE_DIR       Same as --source
  BACKUP_DIR       Same as --destination
  BACKUP_NAME      Same as --name
  EXCLUDE_PATTERN  Same as --exclude
  VERBOSE          Set to 'true' to enable verbose output
EOF
}

## --- Process command line arguments ---
while [[ $## -gt 0 ]]; do
  case $1 in
    -s | --source)
      SOURCE_DIR="$2"
      shift 2
      ;;
    -d | --destination)
      BACKUP_DIR="$2"
      shift 2
      ;;
    -n | --name)
      BACKUP_NAME="$2"
      shift 2
      ;;
    -e | --exclude)
      EXCLUDE_PATTERN="$2"
      shift 2
      ;;
    -v | --verbose)
      VERBOSE="true"
      shift
      ;;
    -h | --help)
      usage
      exit 0
      ;;
    *)
      error "Unknown option: $1"
      ;;
  esac
done

## --- Main function ---
main() {
  ## Validate source directory
  if [[ ! -d "$SOURCE_DIR" ]]; then
    error "Source directory does not exist: $SOURCE_DIR"
  fi

  ## Create backup directory if it doesn't exist
  if [[ ! -d "$BACKUP_DIR" ]]; then
    log "Creating backup directory: $BACKUP_DIR"
    mkdir -p "$BACKUP_DIR" || error "Failed to create backup directory"
  fi

  ## Full path for the backup file
  BACKUP_FILE="$BACKUP_DIR/$BACKUP_NAME.tar.gz"

  log "Starting backup from $SOURCE_DIR to $BACKUP_FILE"
  log "Excluding files matching: $EXCLUDE_PATTERN"

  ## Create the backup
  tar -czf "$BACKUP_FILE" \
    --exclude="$EXCLUDE_PATTERN" \
    -C "$(dirname "$SOURCE_DIR")" "$(basename "$SOURCE_DIR")" \
    || error "Backup failed"

  ## Check if backup was created
  if [[ -f "$BACKUP_FILE" ]]; then
    BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
    echo "Backup completed successfully: $BACKUP_FILE ($BACKUP_SIZE)"
  else
    error "Backup file was not created"
  fi
}

## --- Run main function ---
main
  1. Torne o script executável:
chmod +x backup.sh
  1. Execute o script com as configurações padrão:
./backup.sh

Você deve ver uma mensagem confirmando que o backup foi criado com sucesso.

  1. Verifique o arquivo de backup:
ls -lh /tmp/backups/
  1. Tente executar o script com opções diferentes:
./backup.sh --source ~/project --destination ~/backups --name my_project_backup --verbose

Se o diretório ~/backups não existir, o script o criará. Você pode ver um erro se não tiver permissões de gravação para criar esse diretório.

Testando o Tratamento de Erros em Nosso Script de Backup

Vamos testar como nosso script lida com erros:

  1. Tente fazer backup de um diretório inexistente:
./backup.sh --source /path/that/does/not/exist

Você deve ver uma mensagem de erro como:

ERROR: Source directory does not exist: /path/that/does/not/exist
  1. Tente definir um destino de backup inválido (onde você não tem permissões de gravação):
./backup.sh --destination /root/backups

Você deve ver uma mensagem de erro indicando que o script falhou ao criar o diretório de backup.

Criando um Ambiente de Teste

Vamos criar um ambiente de teste para demonstrar nosso script de backup:

  1. Crie uma estrutura de diretório de teste:
mkdir -p ~/project/test_backup/{docs,images,code}
touch ~/project/test_backup/docs/{readme.md,manual.pdf}
touch ~/project/test_backup/images/{logo.png,banner.jpg}
touch ~/project/test_backup/code/{script.sh,data.tmp,config.json}
  1. Execute o script de backup neste diretório de teste:
./backup.sh --source ~/project/test_backup --exclude "*.tmp" --verbose
  1. Liste os arquivos no backup:
tar -tvf /tmp/backups/backup_*.tar.gz | sort

Você deve ver todos os arquivos, exceto aqueles que correspondem ao padrão de exclusão (*.tmp).

Este script de backup demonstra todos os conceitos que abordamos:

  • Definir valores padrão para variáveis usando expansão de parâmetros
  • Usar o modo estrito para detectar erros
  • Lidar com argumentos de linha de comando e variáveis de ambiente
  • Fornecer feedback ao usuário e mensagens de erro
  • Validar entradas e lidar com casos extremos

Com essas técnicas, você pode escrever scripts Bash robustos que lidam com variáveis não definidas de forma elegante e proporcionam uma melhor experiência ao usuário.

Resumo

Neste laboratório, você aprendeu como lidar com variáveis não definidas em scripts Bash, uma habilidade crucial para escrever scripts de shell confiáveis. Aqui está um resumo do que você realizou:

  1. Criou scripts com declaração e uso adequados de variáveis
  2. Identificou os problemas que as variáveis não definidas podem causar em scripts Bash
  3. Usou verificações condicionais para detectar variáveis não definidas
  4. Aplicou técnicas de expansão de parâmetros para fornecer valores padrão
  5. Implementou o modo estrito (set -euo pipefail) para detectar erros antecipadamente
  6. Criou um modelo de script robusto com tratamento de erros adequado
  7. Construiu um utilitário de backup prático que demonstra as melhores práticas

Ao aplicar essas técnicas, você pode escrever scripts Bash mais confiáveis que lidam com casos extremos de forma elegante e proporcionam uma melhor experiência para os usuários. Lembre-se destes pontos-chave:

  • Sempre considere o que acontece quando as variáveis não são definidas
  • Use a expansão de parâmetros para valores padrão
  • Habilite o modo estrito para scripts críticos
  • Valide as entradas e forneça mensagens de erro úteis
  • Documente o comportamento e os requisitos do seu script

Essas práticas ajudarão você a criar scripts Bash que são mais robustos, fáceis de manter e fáceis de usar.