Bash-скрипты: как написать и запустить, аргументы и ошибки

опубликовано
читать
≈ 9 мин
автор
среда и версия
Debian 12, bash 5.2, ShellCheck 0.9
содержание пройти тест Linux

Bash-скрипт — текстовый файл с командами, которые bash выполняет по очереди. Первая строка #!/bin/bash говорит системе, чем его запускать, chmod +x даёт право на запуск, а ./script.sh запускает. Ниже — путь от первого скрипта до аргументов, условий, циклов и защиты от ошибок, с живым выводом Debian 12 и разбором сообщений, на которых спотыкаются новички.

Linux / 01

Кавычки решают, сколько аргументов получит скрипт

Кавычки решают, сколько аргументов получит скриптКоманда ./args.sh /home/dev/docs "My Files" передаёт скрипту два аргумента: $1 — /home/dev/docs, $2 — My Files, $# равно 2. Без кавычек, в команде ./args.sh /home/dev/docs My Files, bash делит My Files по пробелу: $2 — My, $3 — Files, $# равно 3. $0 в обоих случаях — ./args.sh.переменнаяс кавычкамибез кавычек./args.sh /home/dev/docs "My Files"./args.sh /home/dev/docs My Files$0./args.sh./args.sh$1/home/dev/docs/home/dev/docs$2My FilesMy$3—Files$#23
Пробел разделяет аргументы, кавычки склеивают их обратно. Поэтому путь с пробелом в команде и переменную внутри скрипта всегда пишут в кавычках.

Первый скрипт

Создай файл hello.sh с двумя строками:

#!/bin/bash
echo "Привет, $(whoami)! Сегодня $(date +%F)."

Первая строка, shebang, называет интерпретатор: файл выполнит /bin/bash. $(команда) подставляет в текст вывод другой команды. Запустить файл как программу пока не выйдет, причём по двум разным причинам:

./hello.sh
hello.sh
bash: ./hello.sh: Permission denied
bash: hello.sh: command not found

Первая ошибка — у файла нет права на выполнение. Вторая — без ./ bash ищет команду только в каталогах из PATH, а текущего каталога там нет. Скрипт можно отдать интерпретатору напрямую, тогда право на выполнение не нужно:

bash hello.sh
Привет, dev! Сегодня 2026-10-18.

Или выдать право один раз и запускать как обычную программу. Что значит +x, разобрано в материале о правах доступа.

chmod +x hello.sh
./hello.sh
Привет, dev! Сегодня 2026-10-18.

Переменные

Переменную задают без пробелов вокруг = и читают через $. В двойных кавычках подстановка работает, в одинарных — нет:

#!/bin/bash
name="Anna"
count=$(ls /home/dev/docs | wc -l)
echo "Привет, $name! Файлов в docs: $count"
Привет, Anna! Файлов в docs: 2

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

Аргументы: $1, $2 и $#

Всё, что написано после имени скрипта, попадает в нумерованные переменные. $0 — сам скрипт, $1 — первый аргумент, $2 — второй, $# — сколько их всего. Скрипт args.sh печатает их:

#!/bin/bash
echo "Скрипт: $0"
echo "Аргументов: $#"
echo "Первый: $1"
echo "Второй: $2"

Путь с пробелом остаётся одним аргументом, только если он в кавычках:

./args.sh /home/dev/docs "My Files"
./args.sh /home/dev/docs My Files
Скрипт: ./args.sh
Аргументов: 2
Первый: /home/dev/docs
Второй: My Files
Скрипт: ./args.sh
Аргументов: 3
Первый: /home/dev/docs
Второй: My

Внутри скрипта действует то же правило: "$1" в кавычках — одно значение, $1 без кавычек bash снова разрежет по пробелам. Все аргументы целиком передаёт дальше "$@". Значение по умолчанию задаёт ${2:-/home/dev/backups}: если второго аргумента нет, подставится путь после :-.

Условия и коды возврата

Каждая команда, завершаясь, сообщает код: 0 — успех, любое другое число — ошибка. Последний код лежит в $?:

ls /home/dev/nope
echo $?
ls: cannot access '/home/dev/nope': No such file or directory
2

На кодах держатся && и ||: первая запускает вторую команду при успехе, вторая — при ошибке.

mkdir -p /home/dev/backups && echo "каталог готов"
cp /home/dev/nope /tmp/ || echo "не скопировал"
каталог готов
cp: cannot stat '/home/dev/nope': No such file or directory
не скопировал

Условие в bash пишут через if и двойные квадратные скобки. Скрипт check-fixed.sh здоровается с администратором отдельно:

#!/bin/bash
name=$1
if [[ "$name" == "admin" ]]; then
    echo "Привет, администратор"
else
    echo "Привет, ${name:-гость}"
fi
./check-fixed.sh admin
./check-fixed.sh "Anna Petrova"
./check-fixed.sh
Привет, администратор
Привет, Anna Petrova
Привет, гость

Самые нужные проверки внутри [[ ]]:

ПроверкаИстина, когда
-d "$path"это существующий каталог
-f "$path"это существующий обычный файл
-z "$text"строка пустая
-n "$text"строка непустая
"$a" == "$b"строки совпадают
$n -lt 10число меньше 10; ещё есть -le, -gt, -ge, -eq, -ne

Циклы и функции

for перебирает список, например файлы по шаблону. Переменную цикла берут в кавычки, иначе имя с пробелом развалится на части:

#!/bin/bash
for file in /home/dev/docs/*.txt; do
    echo "файл: $file"
done
файл: /home/dev/docs/notes.txt
файл: /home/dev/docs/План встречи.txt

Повторяющийся кусок выносят в функцию. $* внутри неё — все её аргументы одной строкой:

#!/bin/bash
log() {
    echo "[$(date +%T)] $*"
}

log "начинаю копирование"
log "готово"
[16:34:09] начинаю копирование
[16:34:09] готово

Когда команда в скрипте падает

По умолчанию bash не останавливается на ошибке, а идёт дальше. Скрипт copy-report.sh не нашёл файл и всё равно рапортует об успехе, а код возврата у него 0:

#!/bin/bash
cp /srv/reports/today.csv /home/dev/docs/
echo "Отчёт скопирован"
cp: cannot stat '/srv/reports/today.csv': No such file or directory
Отчёт скопирован

Строка set -e в начале останавливает скрипт на первой упавшей команде. Тот же скрипт с ней завершается после ошибки cp с кодом 1, без ложного «скопирован». Ещё две строгие опции: set -u считает ошибкой обращение к незаданной переменной, а set -o pipefail делает ошибочным весь конвейер, если упала любая его часть. Без неё код конвейера — код последней команды:

grep ERROR /var/log/app.log | wc -l
echo $?
set -o pipefail
grep ERROR /var/log/app.log | wc -l
echo $?
grep: /var/log/app.log: No such file or directory
0
0
grep: /var/log/app.log: No such file or directory
0
2

Поэтому скрипты обычно начинают с set -euo pipefail — все три опции одной строкой. У set -e есть исключения: он не срабатывает внутри условия if и в командах слева от && и ||, поэтому важные проверки всё равно пишут явно.

Временные файлы удаляют через trap: команда в нём выполнится при выходе из скрипта, даже если тот упал. В tmpfile.sh последняя команда false имитирует ошибку:

#!/bin/bash
set -euo pipefail
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
echo "Временный файл: $tmp"
sort /etc/passwd > "$tmp"
false
./tmpfile.sh
echo $?
ls /tmp
Временный файл: /tmp/tmp.0bWwRupTnf
1

Скрипт упал с кодом 1, но ls /tmp ничего не нашёл: trap убрал файл.

Главная мысль: скрипт для работы, а не для опыта, начинается с #!/bin/bash и set -euo pipefail, а каждая переменная в нём стоит в двойных кавычках.

Скрипт целиком: резервная копия каталога

Всё вместе — скрипт backup.sh, который упаковывает каталог в архив с датой. Он проверяет аргументы, пишет ошибки в поток ошибок через >&2 и выходит с разными кодами — так его удобно запускать из cron или таймера systemd, где ошибку увидят только по коду и журналу:

#!/bin/bash
set -euo pipefail

if [[ $# -lt 1 ]]; then
    echo "Использование: $0 КАТАЛОГ [КУДА]" >&2
    exit 1
fi

source_dir=$1
target_dir=${2:-/home/dev/backups}

if [[ ! -d "$source_dir" ]]; then
    echo "Нет каталога: $source_dir" >&2
    exit 2
fi

mkdir -p "$target_dir"
archive="$target_dir/$(basename "$source_dir")-$(date +%F).tar.gz"
tar -czf "$archive" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"
echo "Готово: $archive"

Три запуска — без аргументов, с несуществующим каталогом и с нормальным:

./backup.sh
./backup.sh /home/dev/nope
./backup.sh /home/dev/docs
Использование: ./backup.sh КАТАЛОГ [КУДА]
Нет каталога: /home/dev/nope
Готово: /home/dev/backups/docs-2026-10-18.tar.gz

Коды возврата у первых двух запусков — 1 и 2. Файл с пробелом в имени попал в архив целиком:

tar -tzf /home/dev/backups/docs-$(date +%F).tar.gz
docs/
docs/План встречи.txt
docs/notes.txt

Как найти ошибку: bash -x и ShellCheck

bash -x печатает каждую команду перед выполнением, уже с подставленными значениями. Строки трассировки начинаются с +:

bash -x ./check-fixed.sh admin
+ name=admin
+ [[ admin == \a\d\m\i\n ]]
+ echo 'Привет, администратор'
Привет, администратор

ShellCheck читает скрипт, ничего не запуская, и находит типичные ловушки. В Debian и Ubuntu он ставится пакетом shellcheck. Вот что он говорит о скрипте, который удаляет отчёты, не беря имя файла в кавычки:

shellcheck cleanup.sh
In cleanup.sh line 2:
cd /home/dev/reports
^------------------^ SC2164 (warning): Use 'cd ... || exit' or 'cd ... || return' in case cd fails.

Did you mean: 
cd /home/dev/reports || exit


In cleanup.sh line 4:
    rm $file
       ^---^ SC2086 (info): Double quote to prevent globbing and word splitting.

Did you mean: 
    rm "$file"

For more information:
  https://www.shellcheck.net/wiki/SC2164 -- Use 'cd ... || exit' or 'cd ... |...
  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...

Вторая подсказка — ровно та ошибка, что ломает имена с пробелами. На файле My Report.csv этот скрипт промахивается:

rm: cannot remove 'My': No such file or directory
rm: cannot remove 'Report.csv': No such file or directory

Ошибки, на которых спотыкаются

Permission denied. У файла нет права на выполнение: chmod +x script.sh или запуск через bash script.sh.

command not found. Скрипт запущен по имени без ./, а его каталога нет в PATH. Пиши ./script.sh или положи файл в ~/bin.

Файл сохранён с переводами строк Windows. Редактор Windows ставит в конце строк символ \r, и система ищет интерпретатор /bin/bash\r. В Debian 12 с bash 5.2 ошибка выглядит так:

bash: ./crlf.sh: cannot execute: required file not found

В Ubuntu 22.04 с bash 5.1 тот же файл даёт старую формулировку:

bash: ./crlf.sh: /bin/bash^M: bad interpreter: No such file or directory

Лечится удалением \r: sed -i 's/\r$//' crlf.sh или утилитой dos2unix, а в редакторе стоит включить окончания строк LF.

Пробелы вокруг знака =. Строка name = "Anna" — это вызов команды name с аргументами = и Anna:

./assign.sh: line 2: name: command not found

unary operator expected. В одинарных скобках переменная без кавычек: пустая исчезает, значение с пробелом распадается на два слова. Скрипт check.sh с условием if [ $name = "admin" ] падает и так и так:

./check.sh
./check.sh "Anna Petrova"
./check.sh: line 3: [: =: unary operator expected
./check.sh: line 3: [: too many arguments

Помогают кавычки вокруг переменной или двойные скобки [[ ]], где такого разбиения нет.

unbound variable. Под set -u скрипт обратился к аргументу, которого не передали:

./unset.sh: line 3: $1: unbound variable

Проверяй $# в начале или задавай значение по умолчанию: ${1:-...}.

Скрипт запустили через sh. В Debian и Ubuntu sh — это dash, а не bash. [[ ]], массивы и source он не знает, причём ошибка не всегда останавливает скрипт:

sh check-fixed.sh admin
check-fixed.sh: 3: [[: not found
Привет, admin

Условие сломалось, и скрипт молча ушёл в ветку else. Запускай через bash или ./script.sh с shebang #!/bin/bash. Тот же подвох ждёт в строке crontab: её выполняет /bin/sh.

unexpected end of file. Не закрыт блок: забыт fi, done или кавычка. bash показывает конец файла, а не место, где блок открыт:

./nofi.sh: line 6: syntax error: unexpected end of file

Частые вопросы

Чем ./script.sh отличается от source script.sh

./script.sh запускает отдельный процесс bash: переменные, которые скрипт задаёт, исчезают вместе с ним. source выполняет команды в текущей оболочке. Скрипт setvar.sh задаёт report_dir=/home/dev/reports:

./setvar.sh
echo "каталог: $report_dir"
source setvar.sh
echo "каталог: $report_dir"
каталог: 
каталог: /home/dev/reports

Как запускать свой скрипт из любого каталога

Положи его в ~/bin. В Debian и Ubuntu ~/.profile добавляет этот каталог в PATH при входе, если он существует. Проверить можно в новой оболочке входа:

mkdir -p ~/bin && cp backup.sh ~/bin/
bash -l -c 'command -v backup.sh'
/home/dev/bin/backup.sh

Как передать аргумент с пробелом

В кавычках: ./backup.sh "/home/dev/My Files". А внутри скрипта эту переменную тоже пиши в кавычках — "$1", иначе bash разрежет путь заново, как на схеме выше.

Где потренироваться

Скрипты Bash — тема в карте навыков DevOps, рядом с потоками и конвейерами. Проверить себя по Linux можно в тесте навыков: 8 вопросов, баллы видны без регистрации. Как склеивать команды через | и куда уходят ошибки, разобрано в материале о конвейерах и перенаправлении.

Источники