Введение
Неинициализированные переменные в скриптах Bash могут приводить к неожиданному поведению и ошибкам. В этом руководстве мы рассмотрим основы переменных оболочки, узнаем, как определить, когда переменные не инициализированы (не заданы), и реализуем эффективные стратегии для их обработки. К концу этой лабораторной работы вы сможете писать более надежные скрипты Bash, которые корректно обрабатывают проблемы с переменными.
Создание вашего первого скрипта с переменными оболочки
Переменные оболочки (shell variables) — это именованные области хранения, которые содержат значения в ваших скриптах Bash. На этом шаге мы создадим простой скрипт, чтобы понять, как работают переменные, и увидеть, как они используются.
Понимание переменных оболочки
Переменные в Bash позволяют хранить данные, к которым можно обращаться и которыми можно манипулировать в вашем скрипте. Они необходимы для написания гибких и многократно используемых скриптов.
Давайте начнем с создания нового файла скрипта в нашем каталоге проекта:
Откройте WebIDE и создайте новый файл с именем
variables.shв каталоге/home/labex/projectДобавьте следующее содержимое в файл:
#!/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"
Сохраните файл
Сделайте скрипт исполняемым, выполнив эту команду в терминале:
chmod +x variables.sh
- Запустите ваш скрипт:
./variables.sh
Вы должны увидеть вывод, похожий на этот:
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
Правила именования переменных
При именовании переменных в Bash помните об этих правилах:
- Имена переменных могут содержать буквы, цифры и символы подчеркивания
- Имена переменных не могут начинаться с цифры
- Не допускаются пробелы вокруг знака равенства при присваивании значений
- Имена переменных чувствительны к регистру (NAME и name — разные переменные)
Давайте добавим еще несколько примеров в наш скрипт. Откройте variables.sh снова и добавьте следующее в конец:
## 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"
Сохраните файл и запустите его снова:
./variables.sh
Дополнительный вывод должен выглядеть так:
User: John
Home directory: /home/user
Count: 42
Теперь вы создали свой первый скрипт Bash, который правильно использует переменные!
Встреча с неинициализированными переменными
Теперь, когда мы понимаем основы переменных, давайте рассмотрим, что происходит, когда мы пытаемся использовать переменную, которой не было присвоено значение — неинициализированную переменную.
Что такое неинициализированные переменные?
Неинициализированная переменная, также называемая незаданной переменной, — это переменная, которой не было присвоено значение в вашем скрипте. Когда вы пытаетесь использовать неинициализированную переменную, Bash обычно рассматривает ее как пустую строку, что может привести к тонким ошибкам в ваших скриптах.
Давайте создадим новый скрипт, чтобы увидеть это в действии:
- Создайте новый файл с именем
unbound.shв вашем каталоге проекта со следующим содержимым:
#!/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"
- Сделайте скрипт исполняемым:
chmod +x unbound.sh
- Запустите скрипт:
./unbound.sh
Вы должны увидеть вывод, похожий на этот:
Hello, labex! Your home directory is
Total: 5
Обратите внимание, что $home_dir отображается как пустое пространство, а $count рассматривается как 0 в вычислении. Такое молчаливое поведение может привести к ошибкам, которые трудно найти.
Обнаружение неинициализированных переменных
Давайте изменим наш скрипт, чтобы помочь обнаружить эти неинициализированные переменные. Один из способов сделать это — использовать условные проверки перед использованием переменных:
- Откройте
unbound.shи замените его содержимое на:
#!/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"
- Сохраните файл и запустите его снова:
./unbound.sh
Вывод теперь должен быть:
Warning: home_dir is not set!
Using default: /home/default
Warning: count is not set!
Using default count: 10
Total: 15
Этот подход помогает вам перехватывать неинициализированные переменные и обрабатывать их явно, а не позволять им вызывать молчаливые ошибки.
Тестирование с опцией set -u
Другой способ обнаружения неинициализированных переменных — использовать опцию set -u, которая заставляет Bash немедленно завершать работу при обнаружении неинициализированной переменной:
- Создайте новый файл с именем
strict.shсо следующим содержимым:
#!/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"
- Сделайте его исполняемым:
chmod +x strict.sh
- Запустите скрипт:
./strict.sh
Вы должны увидеть сообщение об ошибке, похожее на:
./strict.sh: line 4: favorite_color: unbound variable
Скрипт немедленно завершает работу на строке 4, когда пытается использовать неинициализированную переменную $favorite_color. Это помогает перехватывать неинициализированные переменные на ранней стадии, предотвращая потенциальные проблемы в дальнейшем в скрипте.
Обработка неинициализированных переменных с помощью расширения параметров
Одной из самых мощных функций Bash для обработки неинициализированных переменных является расширение параметров (parameter expansion). Оно позволяет вам предоставлять значения по умолчанию и выполнять различные операции над переменными.
Использование значений по умолчанию с расширением параметров
Расширение параметров предоставляет несколько способов элегантной обработки неинициализированных переменных. Давайте создадим новый скрипт, чтобы продемонстрировать эти методы:
- Создайте новый файл с именем
parameter_expansion.shсо следующим содержимым:
#!/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..."
- Сделайте скрипт исполняемым:
chmod +x parameter_expansion.sh
- Запустите скрипт:
./parameter_expansion.sh
Вы должны увидеть вывод, похожий на:
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...
Общие шаблоны расширения параметров
Вот сводка шаблонов расширения параметров, которые мы только что использовали:
| Синтаксис | Описание |
|---|---|
${var:-default} |
Использовать значение по умолчанию, если var не задана или равна null |
${var-default} |
Использовать значение по умолчанию, только если var не задана |
${var:=default} |
Присвоить var значение по умолчанию, если она не задана или равна null |
${var:?message} |
Отобразить ошибку, если var не задана или равна null |
Создание практического примера
Давайте создадим более практичный скрипт, который использует расширение параметров для обработки настроек конфигурации:
- Создайте новый файл с именем
config_script.shсо следующим содержимым:
#!/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!"
- Сделайте скрипт исполняемым:
chmod +x config_script.sh
- Запустите скрипт со значениями по умолчанию:
./config_script.sh
- Теперь запустите его снова с некоторыми пользовательскими настройками:
FILE_TYPE="sh" VERBOSE="yes" ./config_script.sh
Вывод должен показать ваши скрипты bash (файлы .sh) в текущем каталоге.
Этот скрипт демонстрирует, как использовать расширение параметров для установки значений по умолчанию для переменных конфигурации, делая ваш скрипт гибким и удобным для пользователя.
Использование строгого режима для надежных скриптов
Чтобы создать более надежные и устойчивые к ошибкам скрипты Bash, многие разработчики используют так называемый «строгий режим» (strict mode). Он включает в себя набор опций Bash, которые помогают выявлять распространенные ошибки, включая неинициализированные переменные.
Что такое строгий режим?
Строгий режим обычно включает в себя включение этих опций в начале вашего скрипта:
set -e: Немедленно завершить работу, если команда завершается с ненулевым статусомset -u: Завершить работу, если ссылаются на неинициализированную переменнуюset -o pipefail: Приводит к сбою конвейера, если какая-либо команда в нем завершается с ошибкой
Давайте посмотрим, как реализовать это в скрипте:
- Создайте новый файл с именем
strict_mode.shсо следующим содержимым:
#!/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"
- Сделайте скрипт исполняемым:
chmod +x strict_mode.sh
- Запустите скрипт:
./strict_mode.sh
Вы должны увидеть:
Starting strict mode script...
Processing input.txt and saving to output.txt
Script completed successfully
- Убедитесь, что выходной файл был создан:
cat output.txt
Вы должны увидеть:
Processed data from input.txt
Тестирование обработки ошибок в строгом режиме
Теперь давайте изменим наш скрипт, чтобы увидеть, как строгий режим обрабатывает ошибки:
- Создайте новый файл с именем
strict_mode_errors.shсо следующим содержимым:
#!/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"
- Сделайте скрипт исполняемым:
chmod +x strict_mode_errors.sh
- Запустите скрипт:
./strict_mode_errors.sh
- Теперь отредактируйте скрипт, чтобы раскомментировать одну из строк с ошибкой (например, строку с
$HOME_DIR), сохраните ее и запустите снова:
./strict_mode_errors.sh
Вы должны увидеть, что скрипт немедленно завершает работу с сообщением об ошибке при обнаружении неинициализированной переменной:
Starting script with error demonstrations...
Example 1: About to use an unbound variable
./strict_mode_errors.sh: line 9: HOME_DIR: unbound variable
Это демонстрирует, как строгий режим помогает выявлять проблемы на ранней стадии вашего скрипта.
Создание шаблона надежного скрипта
Давайте создадим шаблон для надежных скриптов Bash, который вы сможете использовать для будущих проектов:
- Создайте новый файл с именем
robust_script_template.shсо следующим содержимым:
#!/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
- Сделайте скрипт исполняемым:
chmod +x robust_script_template.sh
- Запустите скрипт:
./robust_script_template.sh
- Попробуйте запустить его с включенной отладкой:
DEBUG=true ./robust_script_template.sh
Этот шаблон включает в себя:
- Настройки строгого режима для перехвата ошибок
- Конфигурацию по умолчанию с расширением параметров
- Функции ведения журнала и отладки
- Обработку аргументов командной строки
- Чистую структуру с основной функцией
Вы можете использовать этот шаблон в качестве отправной точки для ваших собственных скриптов Bash, чтобы сделать их более надежными и удобными в обслуживании.
Создание реального приложения
Теперь, когда мы узнали о переменных, неинициализированных переменных, расширении параметров и строгом режиме, давайте объединим все эти концепции в практичный скрипт. Мы создадим простую утилиту резервного копирования файлов, которая демонстрирует лучшие практики обработки переменных в Bash.
Планирование нашего скрипта резервного копирования
Наш скрипт резервного копирования будет:
- Принимать исходный каталог и место назначения резервной копии в качестве входных данных
- Разрешать настройку через переменные окружения или аргументы командной строки
- Грамотно обрабатывать ошибки, используя строгий режим
- Предоставлять полезную обратную связь пользователю
Создание скрипта резервного копирования
- Создайте новый файл с именем
backup.shсо следующим содержимым:
#!/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
- Сделайте скрипт исполняемым:
chmod +x backup.sh
- Запустите скрипт с настройками по умолчанию:
./backup.sh
Вы должны увидеть сообщение, подтверждающее, что резервная копия была успешно создана.
- Проверьте файл резервной копии:
ls -lh /tmp/backups/
- Попробуйте запустить скрипт с разными параметрами:
./backup.sh --source ~/project --destination ~/backups --name my_project_backup --verbose
Если каталог ~/backups не существует, скрипт его создаст. Вы можете увидеть ошибку, если у вас нет прав на запись для создания этого каталога.
Тестирование обработки ошибок в нашем скрипте резервного копирования
Давайте протестируем, как наш скрипт обрабатывает ошибки:
- Попробуйте создать резервную копию несуществующего каталога:
./backup.sh --source /path/that/does/not/exist
Вы должны увидеть сообщение об ошибке, подобное следующему:
ERROR: Source directory does not exist: /path/that/does/not/exist
- Попробуйте установить недопустимое место назначения резервной копии (где у вас нет прав на запись):
./backup.sh --destination /root/backups
Вы должны увидеть сообщение об ошибке, указывающее на то, что скрипту не удалось создать каталог резервной копии.
Создание тестовой среды
Давайте создадим тестовую среду, чтобы продемонстрировать наш скрипт резервного копирования:
- Создайте структуру тестового каталога:
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}
- Запустите скрипт резервного копирования в этом тестовом каталоге:
./backup.sh --source ~/project/test_backup --exclude "*.tmp" --verbose
- Выведите список файлов в резервной копии:
tar -tvf /tmp/backups/backup_*.tar.gz | sort
Вы должны увидеть все файлы, кроме тех, которые соответствуют шаблону исключения (*.tmp).
Этот скрипт резервного копирования демонстрирует все концепции, которые мы рассмотрели:
- Установка значений по умолчанию для переменных с использованием расширения параметров
- Использование строгого режима для перехвата ошибок
- Обработка аргументов командной строки и переменных окружения
- Предоставление обратной связи пользователю и сообщений об ошибках
- Проверка входных данных и обработка крайних случаев
С помощью этих методов вы можете писать надежные скрипты Bash, которые корректно обрабатывают неинициализированные переменные и обеспечивают лучший пользовательский опыт.
Резюме
В этой лабораторной работе вы узнали, как обрабатывать неинициализированные переменные в скриптах Bash, что является критическим навыком для написания надежных скриптов оболочки. Вот краткое изложение того, что вы сделали:
- Создали скрипты с правильным объявлением и использованием переменных
- Определили проблемы, которые могут вызывать неинициализированные переменные в скриптах Bash
- Использовали условные проверки для обнаружения неинициализированных переменных
- Применили методы расширения параметров для предоставления значений по умолчанию
- Реализовали строгий режим (
set -euo pipefail) для раннего выявления ошибок - Создали шаблон надежного скрипта с правильной обработкой ошибок
- Создали практичную утилиту резервного копирования, демонстрирующую лучшие практики
Применяя эти методы, вы можете писать более надежные скрипты Bash, которые корректно обрабатывают крайние случаи и обеспечивают лучший опыт для пользователей. Помните эти ключевые моменты:
- Всегда учитывайте, что произойдет, когда переменные не установлены
- Используйте расширение параметров для значений по умолчанию
- Включайте строгий режим для критически важных скриптов
- Проверяйте входные данные и предоставляйте полезные сообщения об ошибках
- Документируйте поведение и требования вашего скрипта
Эти практики помогут вам создавать скрипты Bash, которые будут более надежными, удобными в обслуживании и удобными для пользователя.



