Освоение позиционных параметров: руководство по аргументам Bash-скриптов

Раскройте возможности динамических Bash-скриптов, освоив позиционные параметры. Это подробное руководство объясняет, как получать доступ к аргументам командной строки с помощью `$1`, `$2` и специальных переменных, таких как `$#` (количество аргументов) и критически важной `"$@"` (все аргументы). Изучите основные лучшие практики проверки входных данных, поймите разницу между `$*` и `$@`, а также увидьте практические примеры написания надежных скриптов с обработкой ошибок, которые безупречно адаптируются к пользовательскому вводу.

Освоение позиционных параметров: руководство по аргументам Bash-скриптов

Bash-скрипты становятся гораздо полезнее, когда они принимают аргументы, вместо того чтобы заставлять вас редактировать переменные внутри файла. Скрипт резервного копирования должен принимать исходную директорию. Скрипт развертывания должен принимать имя окружения. Скрипт очистки должен принимать один или несколько путей. Эти значения поступают в виде позиционных параметров: $1, $2, $3 и так далее.

Сложность заключается не в чтении $1. Сложность заключается в обработке отсутствующих аргументов, аргументов с пробелами, необязательных флагов и того момента, когда ваш скрипт вырастает из стадии "только для меня" во что-то, что другой человек будет запускать в 2 часа ночи.


Анатомия позиционных параметров

Позиционные параметры — это специальные переменные, определяемые оболочкой, которые соответствуют словам, указанным в командной строке после имени скрипта. Они нумеруются последовательно, начиная с 1.

Параметр Описание Пример значения (при запуске ./script.sh file1 dir/)
$0 Имя самого скрипта (или функции). ./script.sh
$1 Первый аргумент, переданный скрипту. file1
$2 Второй аргумент, переданный скрипту. dir/
$N N-й аргумент (где N > 0).
${10} Аргументы, начиная с 10, должны быть заключены в фигурные скобки.

Доступ к аргументам за пределами $9

В то время как к аргументам с 1 по 9 можно обращаться напрямую как $1...$9, для доступа к десятому аргументу и последующим требуется заключать номер в фигурные скобки, чтобы избежать неоднозначности с переменными окружения или строковыми операциями (например, ${10} вместо $10).


Важные специальные параметры для написания скриптов

Помимо числовых параметров, Bash предоставляет несколько критически важных специальных переменных, относящихся ко всему набору аргументов. Они незаменимы для проверки и итерации.

Подсчет аргументов с помощью $#

Специальная переменная $# содержит общее количество аргументов командной строки, переданных скрипту (исключая $0). Это, пожалуй, самая важная переменная для реализации проверки входных данных.

#!/bin/bash

if [ "$#" -eq 0 ]; then
    echo "Ошибка: Аргументы не предоставлены."
    echo "Использование: $0 <входной_файл>"
    exit 1
fi

echo "Вы предоставили $# аргументов."

Все аргументы: $@ и $*

Переменные $@ и $* обе представляют полный список аргументов, но ведут себя по-разному, особенно в кавычках.

$* (Одна строка)

При заключении в двойные кавычки ("$*") весь список позиционных параметров обрабатывается как один аргумент, разделенный первым символом переменной IFS (Internal Field Separator, обычно пробел).

  • Если входные аргументы: arg1 arg2 arg3
  • "$*" раскрывается в: "arg1 arg2 arg3" (один элемент)

$@ (Отдельные строки — предпочтительно)

При заключении в двойные кавычки ("$@") каждый позиционный параметр обрабатывается как отдельный, заключенный в кавычки аргумент. Это стандартный и предпочтительный метод для перебора аргументов, так как он корректно сохраняет аргументы, содержащие пробелы.

  • Если входные аргументы: arg1 "arg with space" arg3
  • "$@" раскрывается в: "arg1" "arg with space" "arg3" (три различных элемента)

Почему важно заключать в кавычки: демонстрация

Рассмотрим скрипт, запущенный с аргументами: ./test.sh 'hello world' file.txt

#!/bin/bash

# Неэкранированный $* разделяет по пробелам и обычно неправилен.
echo "-- Цикл с использованием неэкранированного \$* --"
for item in $*; do
    echo "Элемент: $item"
done

# Экранированный "$@" сохраняет каждый исходный аргумент.
echo "-- Цикл с использованием экранированного \$@ --"
for item in "$@"; do
    echo "Элемент: $item"
done

При запуске ./test.sh 'hello world' file.txt неэкранированный цикл выведет hello и world как отдельные элементы. Цикл с "$@" сохраняет hello world как один аргумент. Эта разница является причиной, по которой опытные пользователи оболочки почти автоматически используют "$@".


Практические методы обработки аргументов

1. Базовый скрипт извлечения аргументов

Этот простой скрипт демонстрирует, как получить доступ к конкретным параметрам и использовать $0 для предоставления полезной обратной связи.

deploy_service.sh:

#!/bin/bash
# Использование: deploy_service.sh <имя_сервиса> <окружение>

SERVICE_NAME="$1"
ENVIRONMENT="$2"

# Проверка (минимум два аргумента)
if [ "$#" -lt 2 ]; then
    echo "Использование: $0 <имя_сервиса> <окружение>"
    exit 1
fi

echo "Начало развертывания для сервиса: $SERVICE_NAME"
echo "Целевое окружение: $ENVIRONMENT"

# Выполнение команды с использованием проверенных параметров
ssh admin@server-"$ENVIRONMENT" "/path/to/start $SERVICE_NAME"

2. Надежная проверка входных данных

Хорошие скрипты всегда проверяют входные данные перед выполнением. Это включает проверку количества ($#) и часто проверку содержимого аргументов (например, является ли аргумент числом или допустимым путем к файлу).

#!/bin/bash

# 1. Проверка количества аргументов (должно быть ровно 3)
if [ "$#" -ne 3 ]; then
    echo "Ошибка: Этот скрипт требует три аргумента (источник, назначение, пользователь)."
    echo "Использование: $0 <путь_ист> <путь_назн> <пользователь>"
    exit 1
fi

SRC_PATH="$1"
DEST_PATH="$2"
USER="$3"

# 2. Проверка содержимого (Пример: Проверка существования исходного пути)
if [ ! -f "$SRC_PATH" ]; then
    echo "Ошибка: Исходный файл '$SRC_PATH' не найден или не является файлом."
    exit 2
fi

# Если проверка пройдена, продолжаем
echo "Копирование $SRC_PATH в $DEST_PATH от пользователя $USER..."

Совет по лучшим практикам: Всегда предоставляйте четкое и краткое сообщение Использование: при неудачной проверке. Это помогает пользователям быстро исправить вызов команды.

3. Перебор аргументов с помощью shift

Команда shift — это отличный инструмент для последовательной обработки аргументов, часто используемый при обработке простых флагов или при обработке аргументов один за другим внутри цикла while.

shift отбрасывает текущий аргумент $1, перемещает $2 в $1, $3 в $2 и уменьшает $# на единицу. Это позволяет вам обработать первый аргумент и затем повторять цикл, пока не останется аргументов.

#!/bin/bash

# Обработка простого флага -v и затем перечисление оставшихся файлов

VERBOSE=false

if [ "$1" = "-v" ]; then
    VERBOSE=true
    shift  # Отбрасываем флаг -v и сдвигаем аргументы вверх
fi

if $VERBOSE; then
    echo "Подробный режим включен."
fi

if [ "$#" -eq 0 ]; then
    echo "Файлы не указаны."
    exit 0
fi

echo "Обработка $# оставшихся файлов:"
for file in "$@"; do
    if $VERBOSE; then
        echo "Проверка файла: $file"
    fi
    # ... логика обработки здесь
done

Примечание: shift полезен для простого разбора. Для сложных скриптов с множеством флагов обычно лучше подходит getopts для коротких опций. Обработка длинных опций зависит от платформы, поэтому тщательно тестируйте, если используете внешний getopt.

Более реалистичный парсер

Многие внутренние скрипты начинаются с одного необязательного флага и одного обязательного значения. Вот небольшой шаблон, который остается читаемым:

#!/usr/bin/env bash
set -u

dry_run=false
environment=""

usage() {
    echo "Использование: $0 [--dry-run] --env <dev|staging|prod> <файл>..." >&2
}

while [ "$#" -gt 0 ]; do
    case "$1" in
        --dry-run)
            dry_run=true
            shift
            ;;
        --env)
            if [ "$#" -lt 2 ]; then
                echo "Ошибка: --env требует значение." >&2
                usage
                exit 2
            fi
            environment="$2"
            shift 2
            ;;
        --help|-h)
            usage
            exit 0
            ;;
        --)
            shift
            break
            ;;
        -*)
            echo "Ошибка: неизвестная опция: $1" >&2
            usage
            exit 2
            ;;
        *)
            break
            ;;
    esac
done

if [ -z "$environment" ]; then
    echo "Ошибка: --env обязателен." >&2
    usage
    exit 2
fi

if [ "$#" -eq 0 ]; then
    echo "Ошибка: укажите хотя бы один файл." >&2
    usage
    exit 2
fi

for file in "$@"; do
    if [ ! -f "$file" ]; then
        echo "Ошибка: файл не найден: $file" >&2
        exit 3
    fi

    if $dry_run; then
        echo "Будет загружен $file в $environment"
    else
        echo "Загрузка $file в $environment"
        # здесь команда загрузки
    fi
done

Обратите внимание на скучные детали. Сообщения об ошибках отправляются в stderr. -- означает "прекратить разбор опций", что позволяет передать имя файла, начинающееся с дефиса. Финальный цикл по файлам использует "$@", так что release notes.txt остается одним именем файла.

Распространенные ошибки

Самая распространенная ошибка — забывать кавычки:

cp $1 $2

Это ломается, когда любой из путей содержит пробелы или символы подстановки оболочки. Используйте:

cp -- "$1" "$2"

-- сообщает многим командам, что разбор опций завершен, что помогает, если путь начинается с -.

Другая распространенная ошибка — проверка слишком поздно. Если ваш скрипт ожидает два аргумента, проверьте это до выполнения каких-либо разрушительных действий:

if [ "$#" -ne 2 ]; then
    echo "Использование: $0 <источник> <назначение>" >&2
    exit 2
fi

Используйте различные коды завершения, когда это помогает вызывающей стороне. Ошибка использования может быть 2; отсутствующий файл может быть 3; неудачная внешняя команда может сохранить свой собственный статус. Вам не нужна гигантская таксономия кодов завершения, но возврат 0 после неудачного вызова делает автоматизацию менее надежной.

У функций тоже есть позиционные параметры

Внутри функции Bash $1 и $2 относятся к аргументам функции, а не к исходным аргументам скрипта.

log_copy() {
    local src="$1"
    local dest="$2"

    echo "Копирование $src в $dest"
    cp -- "$src" "$dest"
}

log_copy "$1" "$2"

Это полезно, но может удивить, если вы ожидали, что $1 внутри функции будет означать первый аргумент уровня скрипта. Передавайте значения явно. Это делает функцию более тестируемой и более пригодной для повторного использования.

Пересылка аргументов другой команде

Многие скрипты-обертки существуют только для того, чтобы добавить небольшую настройку перед вызовом другой команды. В этом случае "$@" — это то, что делает обертку честной.

#!/usr/bin/env bash
set -e

export APP_ENV=staging
exec /usr/local/bin/myapp "$@"

Если кто-то запускает:

./run-staging.sh --config "config with spaces.yml" --verbose

обернутая команда получает те же три аргумента. Если бы вы использовали $* или неэкранированный $@, путь к конфигу мог бы быть разбит на несколько слов.

exec необязателен, но часто полезен в обертках, потому что он заменяет процесс оболочки целевым процессом. Это делает сигналы более предсказуемыми под управлением systemd, Docker или супервизора процессов.

Значения по умолчанию без сюрпризов

Иногда аргумент должен быть необязательным. Расширение параметров Bash может помочь:

environment="${1:-dev}"

Это означает "использовать $1, если он установлен и не пуст; в противном случае использовать dev". Это нормально для дружественных локальных скриптов, но будьте осторожны с производственными скриптами. Тихий умолчание может развернуть не в то окружение, если кто-то забудет аргумент.

Для рискованных команд предпочитайте явный ввод:

if [ "$#" -lt 1 ]; then
    echo "Использование: $0 <окружение>" >&2
    exit 2
fi

Значения по умолчанию лучше всего, когда последствия невелики, например, установка уровня логирования или выходной директории. Они рискованны, когда аргумент выбирает сервер, удаляет данные или изменяет цель развертывания.

Позиционные параметры и set -u

Многие Bash-скрипты используют set -u, чтобы неопределенные переменные вызывали ошибку. Это может помочь отловить опечатки, но также изменяет поведение при отсутствии позиционных параметров.

#!/usr/bin/env bash
set -u

echo "Первый аргумент: $1"

Запустите этот скрипт без аргументов, и Bash завершится с ошибкой "unbound variable". Эта ошибка технически верна, но недружелюбна. Проверяйте $# перед чтением обязательных параметров:

if [ "$#" -lt 1 ]; then
    echo "Использование: $0 <входной_файл>" >&2
    exit 2
fi

input_file="$1"

Для необязательных параметров при set -u используйте защищенное расширение:

mode="${2:-default}"

Это сохраняет полезность строгого режима, не позволяя отсутствующим необязательным значениям аварийно завершать скрипт.

Когда позиционные параметры — неподходящий интерфейс

Позиционные параметры отлично подходят для небольших команд:

backup.sh /var/www /backup/www.tar.gz

Они становятся трудными для чтения, когда скрипт принимает много значений:

deploy.sh prod us-east-1 api v2.4.1 true false 30

Никто не хочет запоминать, что означает пятый аргумент. Как только скрипт достигает этой точки, используйте именованные флаги или конфигурационный файл:

deploy.sh --env prod --region us-east-1 --service api --version v2.4.1 --timeout 30

Код немного длиннее, но командная строка становится самодокументируемой. Это хороший компромисс для скриптов, используемых командой.

Хорошая обработка позиционных параметров — это в основном дисциплина: проверяйте рано, заключайте в кавычки каждое расширение, если вы намеренно не хотите разделения, используйте "$@" для пересылки аргументов и держите сообщения об использовании рядом с проверками, которые их вызывают. Эти привычки позволяют небольшим скриптам выдерживать настоящие имена файлов, настоящих пользователей и настоящую автоматизацию.