Основы написания сценариев оболочки

ShellBeginner
Практиковаться сейчас

Введение

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

В этой лабораторной работе вы будете постепенно создавать небольшие сценарии Bash. Вы запустите сценарии через Bash и как исполняемые файлы, используете переменные и позиционные аргументы, считаете интерактивный ввод, организуете ветвление с помощью if и проверок файлов, выберете коды завершения, выполните итерации с помощью for и объедините эти приёмы в простом суммаризаторе журналов.

Создайте и запустите первый сценарий

На этом шаге вы создадите сценарий Bash, запустите его через интерпретатор и сделаете его непосредственно исполняемым.

Перейдите в рабочий каталог лабораторной работы:

cd /home/labex/project/shell-lab

Создайте hello.sh с помощью here-документа. Первая строка — это шебанг: он выбирает Bash, когда файл запускается напрямую:

cat > hello.sh <<'EOF'
#!/bin/bash

echo "Hello from a Bash script"
pwd
EOF

Проверьте содержимое файла перед запуском:

cat hello.sh

Явно передайте файл Bash. Это работает даже до добавления разрешения на выполнение:

bash hello.sh

Добавьте разрешение на выполнение и проверьте режим доступа:

chmod u+x hello.sh
ls -l hello.sh

Запустите файл из текущего каталога. ./ задаёт путь к файлу; обычно оболочка не ищет команды в текущем каталоге автоматически:

./hello.sh

Используйте переменные и безопасные кавычки

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

Создайте сценарий с двумя переменными:

cat > variables.sh <<'EOF'
#!/bin/bash

course="Linux for Noobs"
workspace="/home/labex/project/shell-lab"

echo "Course: $course"
echo "Workspace: $workspace"
printf 'Quoted argument count: %s\n' "$#"
printf 'First argument: %s\n' "${1:-not provided}"
EOF

В присваивании оболочке вокруг = не ставят пробелы. Двойные кавычки позволяют раскрыть $course, сохранив Linux for Noobs как одно значение.

Запустите сценарий с одним аргументом, содержащим пробелы:

bash variables.sh "beginner learner"

Специальная переменная $# содержит количество позиционных аргументов, а $1 — первый аргумент. ${1:-not provided} подставляет резервное значение, если первый аргумент отсутствует.

Сохраните вывод для проверки:

bash variables.sh "beginner learner" > variable-output.txt
cat variable-output.txt

Проверяйте позиционные аргументы

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

Создайте greet-argument.sh:

cat > greet-argument.sh <<'EOF'
#!/bin/bash

if [ "$#" -ne 1 ]; then
  echo "Usage: $0 NAME" >&2
  exit 2
fi

name=$1
echo "Hello, $name"
exit 0
EOF

Тестовая команда [ ... ] проверяет, не отличается ли количество аргументов от единицы. >&2 отправляет текст с подсказкой в стандартный поток ошибок, а exit 2 сообщает о некорректном использовании сценария.

Запустите сценарий без аргумента, а затем сразу выведите $? — код завершения предыдущей команды:

bash greet-argument.sh
echo "missing argument status: $?"

Ожидаемый код — 2. Теперь запустите сценарий правильно:

bash greet-argument.sh "Ada Lovelace"
echo "valid argument status: $?"

Сохраните приветствие при успешном выполнении:

bash greet-argument.sh "Ada Lovelace" > argument-greeting.txt

Считывайте интерактивный ввод

На этом шаге вы выведете приглашение пользователю, безопасно считаете одну строку и обработаете пустой ввод.

Создайте ask-name.sh:

cat > ask-name.sh <<'EOF'
#!/bin/bash

read -r -p "Enter your name: " name

if [ -z "$name" ]; then
  echo "A name is required" >&2
  exit 1
fi

echo "Welcome, $name"
EOF

read -r считывает строку, не обрабатывая обратные косые черты особым образом. -p выводит приглашение, а -z проверяет, является ли значение в кавычках пустым.

Запустите сценарий:

bash ask-name.sh

В ответ на приглашение введите:

Grace Hopper

Вы должны увидеть Welcome, Grace Hopper. Чтобы получить воспроизводимый сохранённый результат, передайте одну строку ввода в тот же сценарий через конвейер:

printf 'Grace Hopper\n' | bash ask-name.sh > interactive-greeting.txt
cat interactive-greeting.txt

Организуйте ветвление с помощью проверок файлов

На этом шаге вы используете if, elif и else вместе с проверками файлов, чтобы определить тип пути.

Создайте classify-path.sh:

cat > classify-path.sh <<'EOF'
#!/bin/bash

path=${1:-}

if [ -z "$path" ]; then
  echo "Usage: $0 PATH" >&2
  exit 2
elif [ -d "$path" ]; then
  echo "directory: $path"
elif [ -f "$path" ]; then
  echo "file: $path"
else
  echo "missing: $path" >&2
  exit 1
fi

exit 0
EOF

-d проверяет, является ли путь каталогом, а -f — обычным файлом. Порядок проверок важен: сначала обрабатывается некорректное использование, а затем определяется тип пути.

Проверьте все три возможных результата:

bash classify-path.sh logs
bash classify-path.sh notes.txt
bash classify-path.sh missing.txt
echo "missing path status: $?"

Сохраните успешные результаты вместе. && запускает вторую команду только в том случае, если первая завершилась успешно:

bash classify-path.sh logs > path-results.txt && bash classify-path.sh notes.txt >> path-results.txt
cat path-results.txt

Обработайте несколько файлов с помощью цикла for

На этом шаге вы переберёте файлы журналов и подсчитаете совпадающие записи в каждом из них.

Создайте count-errors.sh:

cat > count-errors.sh <<'EOF'
#!/bin/bash

log_dir=${1:-logs}

if [ ! -d "$log_dir" ]; then
  echo "Log directory not found: $log_dir" >&2
  exit 1
fi

for log_file in "$log_dir"/*.log; do
  error_count=$(grep -c '^ERROR ' "$log_file" || true)
  printf '%s errors=%s\n' "$(basename "$log_file")" "$error_count"
done
EOF

На каждой итерации цикла переменная log_file получает один найденный путь. Кавычки вокруг "$log_file" защищают пробелы в пути, а basename удаляет префикс каталога. grep -c подсчитывает совпадающие строки, а || true не позволяет отсутствию совпадений остановить более крупный рабочий процесс со строгой обработкой ошибок.

Запустите цикл и сохраните его вывод:

bash count-errors.sh logs | tee error-counts.txt

Вы должны увидеть один результат для каждого файла .log.

Объедините проверки, циклы и коды завершения

На этом шаге вы создадите один повторно используемый сценарий, который проверяет каталог, суммирует его журналы и явно сообщает об успехе или ошибке.

Создайте summarize-logs.sh:

cat > summarize-logs.sh <<'EOF'
#!/bin/bash

input_dir=${1:-}
output_file=${2:-}

if [ -z "$input_dir" ] || [ -z "$output_file" ]; then
  echo "Usage: $0 INPUT_DIR OUTPUT_FILE" >&2
  exit 2
fi

if [ ! -d "$input_dir" ]; then
  echo "Input directory not found: $input_dir" >&2
  exit 1
fi

: > "$output_file"

for log_file in "$input_dir"/*.log; do
  lines=$(wc -l < "$log_file")
  errors=$(grep -c '^ERROR ' "$log_file" || true)
  printf '%s lines=%s errors=%s\n' "$(basename "$log_file")" "$lines" "$errors" >> "$output_file"
done

echo "Wrote summary to $output_file"
exit 0
EOF

Команда : > "$output_file" безопасно создаёт файл назначения или очищает его перед началом цикла. Запустите сценарий с обоими обязательными аргументами:

chmod u+x summarize-logs.sh
./summarize-logs.sh logs log-summary.txt
echo "summary status: $?"
cat log-summary.txt

Проверьте ветку обработки ошибки с отсутствующим каталогом:

./summarize-logs.sh missing log-summary.txt
echo "missing directory status: $?"

При успешном выполнении код должен быть 0, а при отсутствии каталога — 1.

Итоги

Вы создали и запустили сценарии Bash, использовали шебанги и разрешения, раскрывали переменные в кавычках, считывали позиционные и интерактивные данные и возвращали содержательные коды завершения. Кроме того, вы организовывали ветвление с помощью if/elif/else, проверяли файлы и каталоги, перебирали журналы циклом for и объединили эти приёмы в повторно используемом суммаризаторе.

Эти основы помогают писать автоматизацию, которую проще читать и безопаснее запускать повторно, а другим командам и заданиям планировщика — проще оценивать.