Bash スクリプトにおける未定義の変数のトラブルシューティング方法

ShellShellBeginner
今すぐ練習

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

Introduction

Unbound variables in Bash scripts can lead to unexpected behavior and errors. In this tutorial, we will explore the fundamentals of shell variables, learn how to identify when variables are unbound (not set), and implement effective strategies to handle them. By the end of this lab, you will be able to write more reliable Bash scripts that handle variable issues gracefully.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL shell(("Shell")) -.-> shell/VariableHandlingGroup(["Variable Handling"]) shell(("Shell")) -.-> shell/ControlFlowGroup(["Control Flow"]) shell(("Shell")) -.-> shell/FunctionsandScopeGroup(["Functions and Scope"]) shell(("Shell")) -.-> shell/SystemInteractionandConfigurationGroup(["System Interaction and Configuration"]) shell/VariableHandlingGroup -.-> shell/variables_decl("Variable Declaration") shell/VariableHandlingGroup -.-> shell/variables_usage("Variable Usage") shell/VariableHandlingGroup -.-> shell/param_expansion("Parameter Expansion") shell/ControlFlowGroup -.-> shell/if_else("If-Else Statements") shell/FunctionsandScopeGroup -.-> shell/func_def("Function Definition") shell/SystemInteractionandConfigurationGroup -.-> shell/exit_status_checks("Exit Status Checks") shell/SystemInteractionandConfigurationGroup -.-> shell/shell_options("Shell Options and Attributes") subgraph Lab Skills shell/variables_decl -.-> lab-400168{{"Bash スクリプトにおける未定義の変数のトラブルシューティング方法"}} shell/variables_usage -.-> lab-400168{{"Bash スクリプトにおける未定義の変数のトラブルシューティング方法"}} shell/param_expansion -.-> lab-400168{{"Bash スクリプトにおける未定義の変数のトラブルシューティング方法"}} shell/if_else -.-> lab-400168{{"Bash スクリプトにおける未定義の変数のトラブルシューティング方法"}} shell/func_def -.-> lab-400168{{"Bash スクリプトにおける未定義の変数のトラブルシューティング方法"}} shell/exit_status_checks -.-> lab-400168{{"Bash スクリプトにおける未定義の変数のトラブルシューティング方法"}} shell/shell_options -.-> lab-400168{{"Bash スクリプトにおける未定義の変数のトラブルシューティング方法"}} end

Creating Your First Script with Shell Variables

Shell variables are named storage locations that hold values in your Bash scripts. In this step, we will create a simple script to understand how variables work and see how they are used.

Understanding Shell Variables

Variables in Bash allow you to store data that can be referenced and manipulated throughout your script. They are essential for writing flexible and reusable scripts.

Let's start by creating a new script file in our project directory:

  1. Open the WebIDE and create a new file named variables.sh in the /home/labex/project directory

  2. Add the following content to the file:

#!/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. Save the file

  2. Make the script executable by running this command in the terminal:

chmod +x variables.sh
  1. Run your script:
./variables.sh

You should see output similar to this:

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

Variable Naming Rules

When naming variables in Bash, keep these rules in mind:

  • Variable names can contain letters, numbers, and underscores
  • Variable names cannot start with a number
  • No spaces are allowed around the equals sign when assigning values
  • Variable names are case-sensitive (NAME and name are different variables)

Let's add a few more examples to our script. Open variables.sh again and add the following at the end:

## 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"

Save the file and run it again:

./variables.sh

The additional output should look like:

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

Now you have created your first Bash script that uses variables correctly!

Encountering Unbound Variables

Now that we understand the basics of variables, let's explore what happens when we try to use a variable that hasn't been assigned a value - an unbound variable.

What are Unbound Variables?

An unbound variable, also called an unset variable, is one that has not been assigned a value in your script. When you try to use an unbound variable, Bash typically treats it as an empty string, which can cause subtle bugs in your scripts.

Let's create a new script to see this in action:

  1. Create a new file named unbound.sh in your project directory with the following content:
#!/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. Make the script executable:
chmod +x unbound.sh
  1. Run the script:
./unbound.sh

You should see output similar to this:

Hello, labex! Your home directory is

Total: 5

Notice that $home_dir appears as a blank space, and $count is treated as 0 in the calculation. This silent behavior can lead to bugs that are difficult to find.

Detecting Unbound Variables

Let's modify our script to help detect these unbound variables. One way to do this is to use conditional checks before using the variables:

  1. Open unbound.sh and replace its contents with:
#!/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. Save the file and run it again:
./unbound.sh

The output should now be:

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

This approach helps you catch unbound variables and handle them explicitly rather than letting them cause silent errors.

Testing with the set -u Option

Another way to detect unbound variables is to use the set -u option, which makes Bash exit immediately when it encounters an unbound variable:

  1. Create a new file named strict.sh with the following content:
#!/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. Make it executable:
chmod +x strict.sh
  1. Run the script:
./strict.sh

You should see an error message similar to:

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

The script exits immediately at line 4 when it tries to use the unbound variable $favorite_color. This helps catch unbound variables early, preventing potential issues later in the script.

Handling Unbound Variables with Parameter Expansion

One of the most powerful features of Bash for handling unbound variables is parameter expansion. It allows you to provide default values and perform various operations on variables.

Using Default Values with Parameter Expansion

Parameter expansion provides several ways to handle unbound variables elegantly. Let's create a new script to demonstrate these techniques:

  1. Create a new file named parameter_expansion.sh with the following content:
#!/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. Make the script executable:
chmod +x parameter_expansion.sh
  1. Run the script:
./parameter_expansion.sh

You should see output like:

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...

Common Parameter Expansion Patterns

Here's a summary of the parameter expansion patterns we just used:

Syntax Description
${var:-default} Use default if var is unset or null
${var-default} Use default only if var is unset
${var:=default} Set var to default if it's unset or null
${var:?message} Display error if var is unset or null

Creating a Practical Example

Let's create a more practical script that uses parameter expansion to handle configuration settings:

  1. Create a new file named config_script.sh with the following content:
#!/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. Make the script executable:
chmod +x config_script.sh
  1. Run the script with default values:
./config_script.sh
  1. Now run it again with some custom settings:
FILE_TYPE="sh" VERBOSE="yes" ./config_script.sh

The output should show your bash scripts (.sh files) in the current directory.

This script demonstrates how to use parameter expansion to set default values for configuration variables, making your script flexible and user-friendly.

Using Strict Mode for Robust Scripts

To create more reliable and error-resistant Bash scripts, many developers use what's often called "strict mode." This includes a set of Bash options that help catch common errors, including unbound variables.

What is Strict Mode?

Strict mode typically involves enabling these options at the beginning of your script:

  • set -e: Exit immediately if a command exits with a non-zero status
  • set -u: Exit if an unbound variable is referenced
  • set -o pipefail: Causes a pipeline to fail if any command in it fails

Let's see how to implement this in a script:

  1. Create a new file named strict_mode.sh with the following content:
#!/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. Make the script executable:
chmod +x strict_mode.sh
  1. Run the script:
./strict_mode.sh

You should see:

Starting strict mode script...
Processing input.txt and saving to output.txt
Script completed successfully
  1. Verify that the output file was created:
cat output.txt

You should see:

Processed data from input.txt

Testing Error Handling in Strict Mode

Now let's modify our script to see how strict mode handles errors:

  1. Create a new file named strict_mode_errors.sh with the following content:
#!/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. Make the script executable:
chmod +x strict_mode_errors.sh
  1. Run the script:
./strict_mode_errors.sh
  1. Now, edit the script to uncomment one of the error lines (for example, the line with $HOME_DIR), save it, and run it again:
./strict_mode_errors.sh

You should see that the script exits immediately with an error message when it encounters the unbound variable:

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

This demonstrates how strict mode helps identify issues early in your script.

Creating a Robust Script Template

Let's create a template for robust Bash scripts that you can use for future projects:

  1. Create a new file named robust_script_template.sh with the following content:
#!/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. Make the script executable:
chmod +x robust_script_template.sh
  1. Run the script:
./robust_script_template.sh
  1. Try running it with debug enabled:
DEBUG=true ./robust_script_template.sh

This template includes:

  • Strict mode settings to catch errors
  • Default configuration with parameter expansion
  • Logging and debugging functions
  • Command-line argument processing
  • A clean structure with a main function

You can use this template as a starting point for your own Bash scripts to make them more robust and maintainable.

Creating a Real-World Application

Now that we have learned about variables, unbound variables, parameter expansion, and strict mode, let's combine all these concepts into a practical script. We will create a simple file backup utility that demonstrates best practices for handling variables in Bash.

Planning Our Backup Script

Our backup script will:

  1. Take a source directory and backup destination as inputs
  2. Allow configuration through environment variables or command-line arguments
  3. Handle errors gracefully using strict mode
  4. Provide useful feedback to the user

Creating the Backup Script

  1. Create a new file named backup.sh with the following content:
#!/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. Make the script executable:
chmod +x backup.sh
  1. Run the script with the default settings:
./backup.sh

You should see a message confirming that the backup was created successfully.

  1. Check the backup file:
ls -lh /tmp/backups/
  1. Try running the script with different options:
./backup.sh --source ~/project --destination ~/backups --name my_project_backup --verbose

If the ~/backups directory doesn't exist, the script will create it. You might see an error if you don't have write permissions to create that directory.

Testing Error Handling in Our Backup Script

Let's test how our script handles errors:

  1. Try to backup a non-existent directory:
./backup.sh --source /path/that/does/not/exist

You should see an error message like:

ERROR: Source directory does not exist: /path/that/does/not/exist
  1. Try to set an invalid backup destination (where you don't have write permissions):
./backup.sh --destination /root/backups

You should see an error message indicating that the script failed to create the backup directory.

Creating a Test Environment

Let's create a test environment to demonstrate our backup script:

  1. Create a test directory structure:
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. Run the backup script on this test directory:
./backup.sh --source ~/project/test_backup --exclude "*.tmp" --verbose
  1. List the files in the backup:
tar -tvf /tmp/backups/backup_*.tar.gz | sort

You should see all files except those matching the exclude pattern (*.tmp).

This backup script demonstrates all the concepts we've covered:

  • Setting default values for variables using parameter expansion
  • Using strict mode to catch errors
  • Handling command-line arguments and environment variables
  • Providing user feedback and error messages
  • Validating inputs and handling edge cases

With these techniques, you can write robust Bash scripts that handle unbound variables gracefully and provide a better user experience.

Summary

In this lab, you have learned how to handle unbound variables in Bash scripts, a critical skill for writing reliable shell scripts. Here is a summary of what you have accomplished:

  1. Created scripts with proper variable declaration and usage
  2. Identified the problems that unbound variables can cause in Bash scripts
  3. Used conditional checks to detect unbound variables
  4. Applied parameter expansion techniques to provide default values
  5. Implemented strict mode (set -euo pipefail) to catch errors early
  6. Created a robust script template with proper error handling
  7. Built a practical backup utility that demonstrates best practices

By applying these techniques, you can write more reliable Bash scripts that handle edge cases gracefully and provide a better experience for users. Remember these key points:

  • Always consider what happens when variables are unset
  • Use parameter expansion for default values
  • Enable strict mode for critical scripts
  • Validate inputs and provide useful error messages
  • Document your script's behavior and requirements

These practices will help you create Bash scripts that are more robust, maintainable, and user-friendly.