"Быстрый
</p>
<ul>
<li>Дай права: <strong>chmod +x script.sh</strong></li>
<li>Запусти из текущей папки: <strong>./script.sh</strong></li>
<li>Или без chmod: <strong>bash script.sh</strong></li>
<li>В cron: всегда полные пути, вывод в лог: <strong>0 3 * * * /полный/путь/script.sh >> /var/log/script.log 2>&1</strong></li>
<li>Из Python: <strong>subprocess.run(["bash", "script.sh"])</strong></li>
<li>В <a class="wpil_keyword_link" href="https://it-apteka.com/category/windows-server/" target="_blank" rel="noopener" title="Windows Server" data-wpil-keyword-link="linked" data-wpil-monitor-id="2332">Windows</a> через WSL: <strong>wsl bash script.sh</strong></li>
</ul>
<p>
<h2>Знакомо?</h2>
<p>Пишешь <a class="wpil_keyword_link" href="https://it-apteka.com/category/scripts/" target="_blank" rel="noopener" title="Скрипты" data-wpil-keyword-link="linked" data-wpil-monitor-id="2331">скрипт</a>. В терминале работает идеально. Добавляешь в cron — тишина. Ни ошибки, ни вывода. Скрипт как будто и не запускался.</p>
<p>Или запускаешь через Python — <code>command not found</code>. Хотя та же команда в терминале работает.</p>
<p>Это не магия и не баг <a class="wpil_keyword_link" href="https://it-apteka.com/tag/bash/" target="_blank" rel="noopener" title="Bash" data-wpil-keyword-link="linked" data-wpil-monitor-id="2344">bash</a>. Это три классических грабли на которые наступают все: отсутствие прав на выполнение, другое окружение в cron, относительные пути которые ломаются когда меняется рабочая директория.</p>
<p>В этой статье разберём запуск bash скрипта по всем сценариям: <a class="wpil_keyword_link" href="https://it-apteka.com/category/linux/" target="_blank" rel="noopener" title="Linux" data-wpil-keyword-link="linked" data-wpil-monitor-id="2327">Linux</a>, cron, Python, Windows, Raspberry Pi. Плюс <a href="https://it-apteka.com/troubleshooting-setevyh-problem-15-komand-dlja-diagnostiki-v-windows-i-linux/" title="Troubleshooting сетевых проблем: 15 команд для диагностики в Windows и Linux" target="_blank" rel="noopener" data-wpil-monitor-id="2334">troubleshooting</a> по каждой типичной ошибке — с командами, не с советами «попробуй перезагрузиться.</p>
<p>Что понадобится:</p>
<ul>
<li><a href="https://it-apteka.com/punto-switcher-i-analogi-dlja-windows-macos-i-linux/" title="Punto Switcher и аналоги для Windows, macOS и Linux" target="_blank" rel="noopener" data-wpil-monitor-id="2335">Linux или WSL на Windows</a></li>
<li>Доступ к терминалу</li>
<li>Текстовый редактор (nano сойдёт)</li>
<li>15 минут</li>
</ul>
<h2>Три проблемы которые bash скрипт решает за одно утро</h2>
<p>Прежде чем переходить к командам — посмотрим на задачи. Потому что «как запустить bash скрипт» отвечает не один, а несколько разных вопросов в зависимости от ситуации.</p>
<p>Ситуация первая: у тебя есть набор команд которые ты запускаешь каждый раз руками. Сначала это одна-две команды, потом три, потом пять, потом ты ошибаешься в третьей и переделываешь с нуля. Bash скрипт — это сохранённая последовательность. Один раз написал правильно, больше не ошибаешься.</p>
<p>Ситуация вторая: нужно запускать что-то по расписанию. Бэкап ночью, очистка логов в воскресенье, проверка состояния каждые 10 минут. Руками — забудешь. Cron плюс bash — не забудет. Запись в crontab занимает минуту.</p>
<p>Ситуация третья: Python-приложение или другая система должна вызывать системные команды. Установить пакет, перезапустить сервис, скопировать файлы. Python умеет это через subprocess — но нужно понять как это настроить правильно.</p>
<p>Всё это разные сценарии — и у каждого свои нюансы. В статье разберём все три, плюс Windows через WSL и Raspberry Pi GPIO.</p>
<h2>Почему bash скрипты ломаются по-разному в разных контекстах</h2>
<p>Один и тот же скрипт может работать в одном месте и падать в другом. Это не рандом — у каждого контекста своё окружение.</p>
<p>Интерактивный терминал. Ты залогинился как user. Bash загрузил ~/.bashrc и ~/.bash_profile — там твой PATH, твои алиасы, твои переменные окружения. Команды находятся потому что ~/.local/bin в PATH.</p>
<p>Cron. Запускает задачи с минимальным окружением: PATH=/usr/bin:/bin, и всё. Никаких ~/.bashrc. Никаких пользовательских переменных. Команды которые были в ~./local/bin — не находятся.</p>
<p>Python subprocess. Наследует окружение Python-процесса. Зависит от того как Python запущен: через virtualenv, системный Python, <a class="wpil_keyword_link" href="https://it-apteka.com/tag/docker/" target="_blank" rel="noopener" title="Docker" data-wpil-keyword-link="linked" data-wpil-monitor-id="2328">Docker</a>-контейнер — у каждого своё окружение.</p>
<p>systemd. Чистое окружение, задаётся в файле unit. По умолчанию PATH стандартный системный. Пользовательские переменные — нет.</p>
<p>Из этого следует одно практическое правило: в скриптах предназначенных для <a href="https://it-apteka.com/avtomaticheskoe-obnovlenie-docker-kontejnerov-polnoe-rukovodstvo-i-primery/" title="Автоматическое обновление Docker контейнеров: полное руководство и примеры" target="_blank" rel="noopener" data-wpil-monitor-id="2336">автоматического запуска — всегда полные</a> пути к командам и файлам. Никогда не полагайся на PATH или рабочую директорию.</p>
<h2>Когда bash скрипт спасает: реальные сценарии</h2>
<p>Bash скрипт начинается с усталости делать одно и то же вручную. Три типичных сценария когда он окупается за первый же запуск.</p>
<p>Резервное копирование. Каждый день: подключиться к серверу, выполнить pg_dump, заархивировать, переместить на хранилище, удалить старые копии. Пять шагов руками — это источник ошибок. Пять строк в bash плюс cron — это надёжная процедура которая работает пока ты спишь.</p>
<p>Деплой приложения. Pull из <a class="wpil_keyword_link" href="https://it-apteka.com/tag/git/" target="_blank" rel="noopener" title="Git" data-wpil-keyword-link="linked" data-wpil-monitor-id="2326">git</a>, остановить сервис, установить зависимости, запустить миграции, поднять сервис, проверить статус. Десять команд которые нужно запомнить в правильном порядке. Скрипт делает это воспроизводимым: одна команда, один результат.</p>
<p>Мониторинг. Проверить что сервисы живы, что диск не переполнен, что сертификаты не истекают. Вручную — забудешь. Скрипт в cron — не забудет. Пришлёт уведомление когда что-то пойдёт не так.</p>
<p>Во всех трёх случаях bash работает как связующий слой между инструментами. Он не заменяет Python для сложной логики или Ansible для управления инфраструктурой. Он склеивает утилиты которые уже есть, в нужном порядке, автоматически.</p>
<h2>Что такое bash скрипт и как он устроен</h2>
<p>Bash <a href="https://it-apteka.com/aktivacija-windows-11-i-office-cherez-powershell-komandy-skripty-diagnostika/" title="Активация Windows 11 и Office через PowerShell: команды, скрипты, диагностика" target="_blank" rel="noopener" data-wpil-monitor-id="2343">скрипт — текстовый файл с командами</a>. Теми же командами которые ты набираешь в терминале вручную — только собранными в один файл чтобы запускать их одной командой.</p>
<p>Никакой компиляции. Никаких бинарников. Открываешь в nano, пишешь команды, сохраняешь, запускаешь.</p>
<p>Минимальный рабочий скрипт — три строки:</p>
<pre><code class="language-bash">
#!/bin/bash
echo "Hello Linux"
date
</code></pre>
<p>Первая строка — shebang. Это не комментарий. <code>#!</code> плюс путь к интерпретатору — это инструкция операционной системе: «запускай этот <a href="https://it-apteka.com/programmy-dlja-udalenija-fajlov-kotorye-ne-udaljajutsja-windows-unlocker-lockhunter-i-drugie/" title="Программы для удаления файлов которые не удаляются Windows: Unlocker, LockHunter и другие" target="_blank" rel="noopener" data-wpil-monitor-id="2337">файл вот этой программой»</a>. Без shebang система не знает что с файлом делать. Может открыть в редакторе, может выдать ошибку, может запустить через sh вместо bash — поведение непредсказуемое.</p>
<p><code>/bin/bash</code> — путь к bash. На большинстве Linux-систем он там и живёт. Если не уверен:</p>
<pre><code class="language-bash">
which bash
</code></pre>
<p>Есть более переносимый вариант shebang:</p>
<pre><code class="language-bash">
#!/usr/bin/env bash
</code></pre>
<p><code>env bash</code> ищет bash через переменную PATH, а не по жёсткому пути. Актуально для macOS, FreeBSD, и ситуаций когда bash установлен не в /bin/bash. Для простых домашних скриптов разницы нет. Для скриптов которые будут работать на разных системах — используй <code>env bash</code>.</p>
<h3>Полная структура bash скрипта</h3>
<p>Разберём скелет из которого строится любой рабочий скрипт:</p>
<pre><code class="language-bash">
#!/bin/bash
# Безопасный режим - об этом ниже
set -euo pipefail
# Переменные
NAME="Linux"
DATE=$(date +%Y-%m-%d)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Вывод
echo "Привет, $NAME!"
echo "Сегодня: $DATE"
echo "Скрипт находится в: $SCRIPT_DIR"
# Условие
if [ -f /etc/hosts ]; then
echo "Файл /etc/hosts существует"
else
echo "Файл не найден"
fi
# Цикл
for i in 1 2 3 4; do
echo "Итерация $i"
done
# Функция
greet() {
local name=$1
echo "Привет, $name!"
}
greet "мир"
</code></pre>
<p>Разберём что тут важно.</p>
<p><code>set -euo pipefail</code> — три флага которые делают скрипт предсказуемым. Без них bash по умолчанию продолжает выполнение даже когда команда завершилась с ошибкой. Это нередко приводит к тому что скрипт «выполнился успешно» но половина шагов тихо упала. Подробнее в разделе про типичные ошибки.</p>
<p><code>SCRIPT_DIR</code> — трюк для получения пути к директории скрипта. Нужен чтобы скрипт находил файлы рядом с собой независимо от того откуда его запустили. Пригодится когда будешь добавлять скрипт в cron.</p>
<p><code>$(command)</code> — подстановка вывода команды в переменную. <code>DATE=$(date +%Y-%m-%d)</code> запускает команду date и записывает результат в переменную.</p>
<h2>Запуск bash скрипта в Linux: три способа</h2>
<p>Есть три способа запустить скрипт. Разница — в правах и в том, кто именно его выполняет.</p>
<h3>Шаг 0: дать права на выполнение</h3>
<p>Первое что нужно сделать с новым скриптом — дать ему права на выполнение. Это делается один раз:</p>
<pre><code class="language-bash">
chmod +x script.sh
</code></pre>
<p>Без этого Linux откажется запускать файл как программу — даже если внутри правильный bash-код. Это <a class="wpil_keyword_link" href="https://it-apteka.com/category/security/" target="_blank" rel="noopener" title="Безопасность" data-wpil-keyword-link="linked" data-wpil-monitor-id="2330">защита</a>: не каждый текстовый файл должен быть исполняемым.</p>
<p>Посмотри что изменилось:</p>
<pre><code class="language-bash">
ls -la script.sh
</code></pre>
<p>До chmod: <code>-rw-r--r--</code> — нет символа <code>x</code>, файл нельзя выполнить.<br />
После chmod: <code>-rwxr-xr-x</code> — <code>x</code> появился у владельца, группы и остальных.</p>
<p>Разные варианты chmod:</p>
<pre><code class="language-bash">
chmod +x script.sh # права на выполнение всем
chmod u+x script.sh # только владельцу
chmod 755 script.sh # rwxr-xr-x (то же что chmod +x, числом)
chmod 700 script.sh # rwx------ (только владелец, никто больше не видит)
</code></pre>
<p>Для скрипта который запускает только твой пользователь — <code>chmod 700</code>. Для скрипта в общей директории — <code>chmod 755</code>.</p>
<h3>Способ 1: прямой запуск через ./</h3>
<pre><code class="language-bash">
./script.sh
</code></pre>
<p>Точка и слеш означают: «запустить файл из текущей директории». Нужны потому что bash ищет команды в директориях из переменной PATH — и текущая директория в PATH не входит (по соображениям безопасности). Без <code>./</code> bash скажет <code>command not found</code>.</p>
<p>Права на выполнение нужны.</p>
<h3>Способ 2: через интерпретатор bash</h3>
<pre><code class="language-bash">
bash script.sh
</code></pre>
<p>Явно вызываешь bash и передаёшь ему файл как аргумент. <code>chmod +x</code> не нужен — bash не проверяет флаг исполняемости при таком запуске. Полезно для отладки чужих скриптов и для быстрого запуска без лишних телодвижений.</p>
<h3>Способ 3: по полному пути</h3>
<pre><code class="language-bash">
/home/user/scripts/script.sh
</code></pre>
<p>Нужен когда скрипт запускается не из его директории: из cron, systemd, другого скрипта. Права на выполнение нужны.</p>
<p>Правило: в любой автоматизации — cron, systemd, <a href="https://it-apteka.com/ustanovka-docker-compose-polnyj-gajd/" title="Установка Docker Compose на Linux: полный гайд 2026" target="_blank" rel="noopener" data-wpil-monitor-id="2338">Docker — всегда используй полные</a> пути. Никогда не рассчитывай на рабочую директорию или PATH.</p>
<h3>Быстрая проверка перед запуском</h3>
<p>Прежде чем запускать — проверь синтаксис. Bash это умеет:</p>
<pre><code class="language-bash">
bash -n script.sh
</code></pre>
<p>Флаг <code>-n</code> означает «проверь синтаксис, не выполняй». Если ошибок нет — тишина (хорошо). Если есть — покажет строку и описание ошибки.</p>
<pre><code class="language-bash">
bash -x script.sh
</code></pre>
<p>Флаг <code>-x</code> — режим трассировки. Bash выполняет скрипт и выводит каждую команду перед выполнением. Незаменимо при отладке: видишь что именно выполняется, с какими подставленными значениями переменных.</p>
<h2>Переменные, условия и циклы: практика без учебника</h2>
<p>Bash-скрипт без переменных — это просто список команд. Переменные появляются как только нужно что-то переиспользовать или передать между шагами.</p>
<h3>Переменные: основные правила</h3>
<pre><code class="language-bash">
#!/bin/bash
# Объявление переменной - БЕЗ пробелов вокруг =
NAME="Андрей"
PORT=8080
LOG_DIR="/var/log/myapp"
# Использование - знак $
echo "Привет, $NAME"
echo "Порт: $PORT"
echo "Логи: $LOG_DIR"
# В фигурных скобках - когда переменная идёт вплотную к тексту
FILE_PREFIX="backup"
echo "${FILE_PREFIX}_2024.tar.gz"
# Подстановка вывода команды
CURRENT_DATE=$(date +%Y-%m-%d)
HOSTNAME=$(hostname)
FREE_DISK=$(df -h / | awk 'NR==2 {print $4}')
echo "Дата: $CURRENT_DATE"
echo "Хост: $HOSTNAME"
echo "Свободно на /: $FREE_DISK"
</code></pre>
<p>Пробелы вокруг <code>=</code> — главная ловушка для новичков. <code>NAME = "value"</code> это не присвоение — это команда <code>NAME</code> с аргументами <code>=</code> и <code>"value"</code>. Bash выдаст <code>command not found</code>.</p>
<h3>Специальные переменные</h3>
<pre><code class="language-bash">
#!/bin/bash
# $0 - имя скрипта
# $1, $2, ... - аргументы скрипта
# $# - количество аргументов
# $@ - все аргументы как массив
# $? - код возврата последней команды
# $$ - PID текущего процесса
# $! - PID последнего фонового процесса
echo "Скрипт: $0"
echo "Аргументов: $#"
echo "Первый аргумент: $1"
echo "PID скрипта: $$"
# Проверка что передан нужный аргумент
if [ $# -lt 1 ]; then
echo "Использование: $0 <имя_базы>"
exit 1
fi
DB_NAME=$1
echo "Работаем с базой: $DB_NAME"
</code></pre>
<h3>Условия: if, case, &&, ||</h3>
<pre><code class="language-bash">
#!/bin/bash
FILE="/etc/hosts"
PORT=80
# Проверка существования файла
if [ -f "$FILE" ]; then
echo "$FILE существует"
fi
# Проверка директории
if [ -d "/var/log" ]; then
echo "Директория /var/log есть"
fi
# Сравнение строк
STATUS="running"
if [ "$STATUS" = "running" ]; then
echo "Сервис работает"
elif [ "$STATUS" = "stopped" ]; then
echo "Сервис остановлен"
else
echo "Неизвестный статус: $STATUS"
fi
# Сравнение чисел
if [ $PORT -eq 80 ]; then
echo "Стандартный HTTP порт"
fi
if [ $PORT -gt 1024 ]; then
echo "Непривилегированный порт"
fi
# Короткая форма - && и ||
[ -f /etc/nginx/nginx.conf ] && echo "nginx конфиг найден"
[ -f /etc/nginx/nginx.conf ] || echo "nginx не установлен"
</code></pre>
<p>Квадратные скобки <code>[ ]</code> — это команда <code>test</code>. Пробелы внутри обязательны. <code>[ -f file ]</code> работает. <code>[-f file]</code> — нет.</p>
<p>Двойные скобки <code>[[ ]]</code> — bash-расширение. Поддерживают регулярные выражения, логические операторы без экранирования, не требуют кавычек вокруг переменных. Если пишешь только для bash (shebang <code>#!/bin/bash</code>) — используй <code>[[ ]]</code>.</p>
<h3>Циклы: for, while, until</h3>
<pre><code class="language-bash">
#!/bin/bash
# for - перебор списка
for server in web1 web2 web3; do
echo "Проверяю $server..."
ping -c 1 "$server" > /dev/null 2>&1 && echo " OK" || echo " НЕДОСТУПЕН"
done
# for - перебор файлов
for log_file in /var/log/*.log; do
SIZE=$(du -h "$log_file" | cut -f1)
echo "$log_file: $SIZE"
done
# for - числовой диапазон
for i in {1..10}; do
echo "Итерация $i"
done
# while - пока условие истинно
COUNTER=0
while [ $COUNTER -lt 5 ]; do
echo "Счётчик: $COUNTER"
COUNTER=$((COUNTER + 1))
done
# while - читать файл построчно
while IFS= read -r line; do
echo "Строка: $line"
done < /etc/hosts
</code></pre>
<p>Чтение файла построчно через <code>while read</code> - стандартный идиом bash. <code>IFS=</code> сбрасывает разделитель полей чтобы строки с пробелами читались корректно. <code>-r</code> отключает интерпретацию обратных слешей.</p>
<h3>Heredoc: многострочный ввод и генерация файлов</h3>
<p>Heredoc - способ передать многострочный текст команде или записать в файл прямо в скрипте:</p>
<pre><code class="language-bash">
#!/bin/bash
# Создать конфиг-файл
cat > /etc/myapp/config.ini << 'EOF'
[database]
host = localhost
port = 5432
name = myapp
[server]
port = 8080
debug = false
EOF
echo "Конфиг создан"
# Передать SQL в psql
psql -U admin mydb << 'EOF'
CREATE TABLE IF NOT EXISTS events (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
EOF
echo "Таблица создана"
</code></pre>
<p>Одинарные кавычки вокруг EOF (<code>'EOF'</code>) отключают интерпретацию переменных внутри heredoc - то что написано остаётся как есть. Без кавычек (<code>EOF</code>) - переменные подставляются. Используй одинарные кавычки когда пишешь конфиг с символами доллара, двойные кавычки или обратные слеши.</p>
<h2>Запуск bash скрипта для автоматизации сервисов</h2>
<p>Одно из частых применений bash - запуск нескольких сервисов одной командой. Особенно полезно при старте окружения разработки или при разворачивании стека на сервере.</p>
<h3>Последовательный запуск нескольких программ</h3>
<pre><code class="language-bash">
#!/bin/bash
set -euo pipefail
echo "=== Запуск стека ==="
echo "Время: $(date '+%Y-%m-%d %H:%M:%S')"
# Nginx
systemctl start nginx
echo "[OK] nginx"
# PostgreSQL
systemctl start postgresql
echo "[OK] postgresql"
# Redis
systemctl start redis
echo "[OK] redis"
echo "=== Стек запущен ==="
</code></pre>
<h3>Запуск с проверкой результата</h3>
<pre><code class="language-bash">
#!/bin/bash
set -euo pipefail
start_service() {
local service=$1
systemctl start "$service"
if systemctl is-active --quiet "$service"; then
echo "[OK] $service - запущен"
return 0
else
echo "[FAIL] $service - не запустился"
systemctl status "$service" --no-pager
return 1
fi
}
# Запускаем и проверяем
start_service nginx
start_service postgresql
start_service redis
echo "Все сервисы запущены."
</code></pre>
<p>Функция <code>start_service</code> запускает сервис и сразу проверяет статус через <code>systemctl is-active</code>. Если сервис не поднялся - показывает статус и возвращает код ошибки. При <code>set -e</code> скрипт остановится на первой ошибке - не будет запускать остальные сервисы на неработающем стеке.</p>
<h3>Параллельный запуск с ожиданием</h3>
<pre><code class="language-bash">
#!/bin/bash
# Запустить несколько процессов параллельно
python3 /home/user/app/worker1.py &
WORKER1_PID=$!
python3 /home/user/app/worker2.py &
WORKER2_PID=$!
echo "Воркеры запущены: PID $WORKER1_PID, PID $WORKER2_PID"
# Ждём завершения обоих
wait $WORKER1_PID
echo "Воркер 1 завершён"
wait $WORKER2_PID
echo "Воркер 2 завершён"
</code></pre>
<p>Символ <code>&</code> запускает процесс в фоне. <code>$!</code> - PID последнего запущенного фонового процесса. <code>wait PID</code> блокирует скрипт до завершения процесса с этим PID. Полезно когда нужно параллельно запустить несколько задач и дождаться когда все закончатся.</p>
<h2>Запуск bash скрипта по расписанию через cron</h2>
<p>Cron - встроенный планировщик задач Linux. Каждый пользователь имеет свой crontab - список задач с расписанием. Демон crond читает его и запускает задачи в нужное время.</p>
<h3>Синтаксис crontab</h3>
<pre><code class="language-bash">
# Формат: минуты часы день_месяца месяц день_недели команда
# * * * * * /path/script.sh
# Примеры:
# 0 * * * * - каждый час ровно
# */5 * * * * - каждые 5 минут
# 0 3 * * * - каждый день в 03:00
# 0 3 * * 1 - каждый понедельник в 03:00
# 0 3 1 * * - 1-го числа каждого месяца в 03:00
# 30 8 * * 1-5 - по будням в 8:30
# @reboot - один раз при загрузке системы
</code></pre>
<h3>Открыть редактор crontab</h3>
<pre><code class="language-bash">
crontab -e
</code></pre>
<p>Первый раз cron спросит какой редактор использовать. Если не знаком с vim - выбери nano (обычно цифра 1).</p>
<pre><code class="language-bash">
# Посмотреть текущие задачи
crontab -l
# Удалить все задачи (будет запрос подтверждения)
crontab -r
</code></pre>
<h3>Практические примеры cron задач</h3>
<pre><code class="language-bash">
# Бэкап БД каждую ночь в 3:00
0 3 * * * /home/user/scripts/backup_db.sh >> /var/log/backup_db.log 2>&1
# Очистка временных файлов каждое воскресенье в 4:00
0 4 * * 0 /home/user/scripts/clean_tmp.sh >> /var/log/clean.log 2>&1
# Проверка дискового пространства каждые 10 минут
*/10 * * * * /home/user/scripts/check_disk.sh >> /var/log/disk_check.log 2>&1
# Перезапуск сервиса каждое утро в 6:00
0 6 * * * /bin/systemctl restart myapp >> /var/log/myapp_restart.log 2>&1
# При загрузке системы
@reboot /home/user/scripts/startup.sh >> /var/log/startup.log 2>&1
</code></pre>
<p>Конструкция <code>>> /var/log/script.log 2>&1</code> обязательна для любой cron-задачи которую хочешь контролировать. <code>>></code> дописывает вывод в файл (не перезаписывает). <code>2>&1</code> перенаправляет stderr туда же куда идёт stdout. Без этого все ошибки уходят в никуда - или в почту root, которую никто не читает.</p>
<h3>Почему скрипт не работает в cron хотя вручную работает</h3>
<p>Классика жанра. Скрипт в терминале отработал отлично. В cron - ни звука. Причина в 99% случаев - окружение.</p>
<p>Cron запускает задачи с минимальным окружением. Переменная PATH в cron содержит только <code>/usr/bin:/bin</code>. Никакого <code>/usr/local/bin</code>, никакого пользовательского <code>~/.local/bin</code>. Команда <code>python3</code>, <code>node</code>, <code>pip</code>, <code>docker</code> - могут быть не найдены.</p>
"Три
<br />
1. Полные пути к исполняемым файлам: не python3, а /usr/bin/python3<br />
2. Полные пути к файлам скрипта: не ./backup.sh, а /home/user/scripts/backup.sh<br />
3. Вывод в лог: >> /var/log/script.log 2>&1 — обязательно для отладки<br />
<p>Как найти полный путь команды:</p>
<pre><code class="language-bash">
which python3
# /usr/bin/python3
which node
# /usr/local/bin/node
</code></pre>
<p>Второй способ - добавить нужный PATH прямо в начало скрипта:</p>
<pre><code class="language-bash">
#!/bin/bash
# Явно задаём PATH для cron
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# Теперь python3, node, docker - все будут найдены
python3 /home/user/app/main.py
</code></pre>
<p>Третий способ - задать PATH прямо в crontab, в самом начале, до первой задачи:</p>
<pre><code class="language-bash">
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1
</code></pre>
<h3>Отладка cron задач</h3>
<p>Скрипт в cron не работает, лог пустой. Значит скрипт вообще не запустился - или запустился, но что-то пошло не так ещё до твоих команд.</p>
<p>Первое - проверь что cron вообще запущен:</p>
<pre><code class="language-bash">
systemctl status cron
# или
systemctl status crond # на некоторых дистрибутивах
</code></pre>
<p>Второе - смотри системный лог cron:</p>
<pre><code class="language-bash">
grep CRON /var/log/syslog | tail -20
# или на системах с journald
journalctl -u cron --since "1 hour ago"
</code></pre>
<p>Там будет запись о каждом запуске задачи - даже если скрипт ничего не вывел. Если записи нет - задача не запустилась вообще (проверь синтаксис crontab).</p>
<p>Третье - протестируй скрипт с окружением cron. Запусти скрипт с минимальным PATH вручную:</p>
<pre><code class="language-bash">
env -i PATH=/usr/bin:/bin bash /home/user/scripts/script.sh
</code></pre>
<p><code>env -i</code> запускает команду с пустым окружением. Если скрипт завалился здесь - проблема в PATH или переменных окружения. Если работает - проблема в crontab записи.</p>
<h3>Чек-лист отладки cron задачи</h3>
<p>Скрипт не работает в cron. Пройдись по списку по порядку.</p>
<table>
<thead>
<tr>
<th>Что проверить</th>
<th>Как проверить</th>
<th>Что сделать</th>
</tr>
</thead>
<tbody>
<tr>
<td>cron вообще запущен</td>
<td>systemctl status cron</td>
<td>systemctl start cron</td>
</tr>
<tr>
<td>Задача есть в crontab</td>
<td>crontab -l</td>
<td>crontab -e, добавь задачу</td>
</tr>
<tr>
<td>Синтаксис crontab правильный</td>
<td>Проверь на crontab.guru</td>
<td>Исправь расписание</td>
</tr>
<tr>
<td>Скрипт существует по указанному пути</td>
<td>ls -la /полный/путь/script.sh</td>
<td>Поправь путь в crontab</td>
</tr>
<tr>
<td>Скрипт исполняемый</td>
<td>ls -la script.sh (должен быть x)</td>
<td>chmod +x script.sh</td>
</tr>
<tr>
<td>Лог пишется</td>
<td>cat /var/log/script.log</td>
<td>Добавь >> /var/log/script.log 2>&1</td>
</tr>
<tr>
<td>Системный лог cron</td>
<td>grep CRON /var/log/syslog | tail -20</td>
<td>Смотри ошибки запуска</td>
</tr>
<tr>
<td>Скрипт работает с cron-окружением</td>
<td>env -i PATH=/usr/bin:/bin bash script.sh</td>
<td>Добавь PATH в скрипт или используй полные пути</td>
</tr>
</tbody>
</table>
<h3>Альтернатива cron: systemd timers</h3>
<p>Systemd timers - более современный способ планировки. Плюсы: зависимости между сервисами, логирование через journald, точная настройка тайминга. Минус: сложнее настраивать чем cron.</p>
<p>Создай два файла:</p>
<p>Сервис <code>/etc/systemd/system/backup.service</code>:</p>
<pre><code class="language-text">
[Unit]
Description=Database Backup
[Service]
Type=oneshot
ExecStart=/home/user/scripts/backup_db.sh
User=backup
StandardOutput=journal
StandardError=journal
</code></pre>
<p>Таймер <code>/etc/systemd/system/backup.timer</code>:</p>
<pre><code class="language-text">
[Unit]
Description=Daily Database Backup Timer
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
</code></pre>
<pre><code class="language-bash">
sudo systemctl daemon-reload
sudo systemctl enable backup.timer
sudo systemctl start backup.timer
# Проверить все таймеры
systemctl list-timers
# Логи
journalctl -u backup.service
</code></pre>
<p><code>Persistent=true</code> означает: если система была выключена в момент когда задача должна была запуститься - запустить при следующей загрузке. Cron так не умеет.</p>
<h2>Запуск bash скрипта из Python</h2>
<p>Python и bash - хорошая пара. Python берёт на себя логику, работу с API, обработку данных. Bash - системные операции, управление файлами, запуск утилит. Запустить bash из Python можно тремя способами.</p>
<h3>subprocess.run - правильный способ</h3>
<pre><code class="language-python">
import subprocess
# Запустить скрипт
result = subprocess.run(["bash", "script.sh"])
# Запустить и получить вывод
result = subprocess.run(
["bash", "script.sh"],
capture_output=True,
text=True
)
print("Вывод:", result.stdout)
print("Ошибки:", result.stderr)
print("Код возврата:", result.returncode)
</code></pre>
<p><code>capture_output=True</code> перехватывает stdout и stderr. <code>text=True</code> возвращает строки вместо байт - не надо декодировать вручную.</p>
<p>Запуск произвольной shell-команды строкой:</p>
<pre><code class="language-python">
result = subprocess.run(
"ls -la /home/user | grep -v '^d'",
shell=True,
capture_output=True,
text=True
)
print(result.stdout)
</code></pre>
<p>Параметр <code>shell=True</code> передаёт строку в shell как есть - работают пайпы, перенаправления, glob-шаблоны. Без него нужно передавать список аргументов. Для команд с пайпами и перенаправлениями - используй <code>shell=True</code>. Для безопасного запуска с аргументами от пользователя - список без <code>shell=True</code> (иначе shell injection).</p>
<h3>subprocess.Popen - для долгих процессов</h3>
<p><code>subprocess.run</code> ждёт завершения процесса. Если скрипт долгий и нужно читать его вывод в реальном времени - используй <code>Popen</code>:</p>
<pre><code class="language-python">
import subprocess
process = subprocess.Popen(
["bash", "long_script.sh"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# Читаем вывод построчно по мере поступления
for line in process.stdout:
print(line, end="")
process.wait()
print(f"Завершён с кодом: {process.returncode}")
</code></pre>
<p>Это работает потому что <code>process.stdout</code> - это файлоподобный объект. Итерация по нему блокируется до появления новой строки. Как только скрипт пишет в stdout - Python немедленно это получает и выводит.</p>
<h3>Полная обёртка для продакшна</h3>
<pre><code class="language-python">
import subprocess
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def run_bash_script(script_path, *args, timeout=60):
"""
Запустить bash скрипт и вернуть вывод.
Args:
script_path: полный путь к скрипту
*args: аргументы для скрипта
timeout: таймаут в секундах (по умолчанию 60)
Returns:
str: вывод скрипта (stdout)
Raises:
RuntimeError: если скрипт завершился с ненулевым кодом
subprocess.TimeoutExpired: если превышен таймаут
"""
cmd = ["bash", script_path] + list(args)
logger.info(f"Запуск: {' '.join(cmd)}")
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout
)
except subprocess.TimeoutExpired:
logger.error(f"Таймаут {timeout}с: {script_path}")
raise
if result.returncode != 0:
logger.error(f"Скрипт вернул код {result.returncode}")
logger.error(f"stderr: {result.stderr}")
raise RuntimeError(
f"Скрипт завершился с ошибкой (код {result.returncode}):\n"
f"{result.stderr}"
)
logger.info("Скрипт завершён успешно")
return result.stdout.strip()
# Использование
try:
output = run_bash_script(
"/home/user/scripts/backup.sh",
"--database", "mydb",
"--dest", "/backup"
)
print(f"Бэкап успешен:\n{output}")
except RuntimeError as e:
print(f"Ошибка: {e}")
except subprocess.TimeoutExpired:
print("Скрипт завис - превышен таймаут")
</code></pre>
<h3>subprocess в Python: продвинутые сценарии</h3>
<p>Стандартные случаи покрыты выше. Но есть несколько нюансов которые выплывают в реальных проектах.</p>
<p>Передача stdin скрипту из Python:</p>
<pre><code class="language-python">
import subprocess
# Передать данные в stdin скрипта
result = subprocess.run(
["bash", "process_input.sh"],
input="строка1\nстрока2\nстрока3",
capture_output=True,
text=True
)
print(result.stdout)
</code></pre>
<p>Выполнение bash-команды с переменными окружения:</p>
<pre><code class="language-python">
import subprocess
import os
# Добавить переменные к текущему окружению
env = os.environ.copy()
env["DB_HOST"] = "localhost"
env["DB_PORT"] = "5432"
env["APP_ENV"] = "production"
result = subprocess.run(
["bash", "deploy.sh"],
capture_output=True,
text=True,
env=env
)
</code></pre>
<p>Перехват вывода в реальном времени с таймаутом:</p>
<pre><code class="language-python">
import subprocess
import threading
def stream_output(process):
for line in process.stdout:
print(f"[bash] {line.rstrip()}")
process = subprocess.Popen(
["bash", "long_job.sh"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # stderr в тот же поток что stdout
text=True,
bufsize=1 # построчная буферизация
)
# Читаем вывод в отдельном потоке
reader = threading.Thread(target=stream_output, args=(process,))
reader.start()
try:
process.wait(timeout=300) # ждём максимум 5 минут
except subprocess.TimeoutExpired:
process.kill()
print("Скрипт убит по таймауту")
finally:
reader.join()
print(f"Код выхода: {process.returncode}")
</code></pre>
<p><code>stderr=subprocess.STDOUT</code> перенаправляет stderr в тот же поток что stdout. Удобно когда хочешь видеть весь вывод в порядке появления, а не раздельно.</p>
<p>Асинхронный запуск через asyncio - для современных Python-приложений:</p>
<pre><code class="language-python">
import asyncio
async def run_script(script_path):
proc = await asyncio.create_subprocess_exec(
"bash", script_path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
raise RuntimeError(f"Ошибка: {stderr.decode()}")
return stdout.decode().strip()
# Запуск нескольких скриптов параллельно
async def main():
tasks = [
run_script("/home/user/scripts/backup_db.sh"),
run_script("/home/user/scripts/sync_files.sh"),
run_script("/home/user/scripts/check_certs.sh"),
]
results = await asyncio.gather(*tasks, return_exceptions=True)
for i, result in enumerate(results):
if isinstance(result, Exception):
print(f"Задача {i+1} упала: {result}")
else:
print(f"Задача {i+1}: {result[:50]}")
asyncio.run(main())
</code></pre>
<p>Asyncio полезно когда нужно параллельно <a href="https://it-apteka.com/powershell-skripty-v-windows-kak-sozdat-zapustit-i-avtomatizirovat-vypolnenie/" title="PowerShell скрипты в Windows: как создать, запустить и автоматизировать выполнение" target="_blank" rel="noopener" data-wpil-monitor-id="2339">запустить несколько долгих скриптов</a> из Python-приложения без создания потоков вручную.</p>
<h3>os.system - когда нужно просто запустить</h3>
<pre><code class="language-python">
import os
# Запустить - возвращает только код возврата
exit_code = os.system("bash script.sh")
if exit_code != 0:
print(f"Ошибка: код {exit_code}")
</code></pre>
<p><code>os.system</code> простой, но не даёт перехватить вывод. Скрипт пишет прямо в терминал. Для новых проектов используй <code>subprocess</code> - гибче и даёт контроль над stdout/stderr. <code>os.system</code> оставь для быстрых однострочников.</p>
<h2>Запуск bash скриптов на Raspberry Pi</h2>
<p>Raspberry Pi работает под Raspberry Pi OS - это <a class="wpil_keyword_link" href="https://it-apteka.com/tag/debian/" target="_blank" rel="noopener" title="Debian" data-wpil-keyword-link="linked" data-wpil-monitor-id="2333">Debian</a>. Всё описанное выше работает напрямую. Но у RPi есть своя специфика - управление GPIO через bash.</p>
<h3>Управление GPIO через bash</h3>
<p>Для работы с GPIO через bash нужна утилита <code>gpio</code> из пакета wiringpi:</p>
<pre><code class="language-bash">
# Установка
sudo apt install wiringpi
# Проверить установку
gpio -v
</code></pre>
"Внимание:
<br />
WiringPi официально прекратил развитие, но неофициальные форки продолжают поддерживаться. На Raspberry Pi 4 и 5 используй форк с github.com/WiringPi/WiringPi. Для более современного подхода — библиотека pigpio или raspi-gpio.<br />
<p>Основные команды:</p>
<pre><code class="language-bash">
# Настроить пин 17 как выход
gpio -g mode 17 out
# Установить HIGH (3.3V)
gpio -g write 17 1
# Установить LOW (0V)
gpio -g write 17 0
# Прочитать состояние пина
gpio -g read 17
# Настроить пин как вход с подтягивающим резистором
gpio -g mode 18 in
gpio -g mode 18 up # pull-up
</code></pre>
<p>Флаг <code>-g</code> означает использование BCM-нумерации пинов (GPIO номера). Без него используется нумерация WiringPi - другая схема. BCM-нумерация указана на распиновке Raspberry Pi и используется во всей документации - используй <code>-g</code>.</p>
<h3>Мигание светодиода</h3>
<pre><code class="language-bash">
#!/bin/bash
PIN=17
# Инициализация пина
gpio -g mode $PIN out
# Корректное завершение по Ctrl+C
cleanup() {
gpio -g write $PIN 0
echo ""
echo "Остановлено. Светодиод выключен."
exit 0
}
trap cleanup SIGINT SIGTERM
echo "Мигание на пине $PIN. Ctrl+C для остановки."
while true; do
gpio -g write $PIN 1
sleep 0.5
gpio -g write $PIN 0
sleep 0.5
done
</code></pre>
<p>Конструкция <code>trap cleanup SIGINT SIGTERM</code> - обработчик сигналов. Когда нажимаешь Ctrl+C, bash получает SIGINT. Без trap скрипт завершится немедленно, светодиод останется в последнем состоянии. С trap перед завершением выполняется функция cleanup - выключает пин. Всегда добавляй trap в GPIO-скрипты.</p>
<h3>Скрипт мониторинга температуры</h3>
<pre><code class="language-bash">
#!/bin/bash
THRESHOLD=70
LOG="/var/log/rpi_temp.log"
get_temp() {
vcgencmd measure_temp | grep -oP '\d+\.\d+'
}
log_temp() {
local temp=$1
echo "$(date '+%Y-%m-%d %H:%M:%S') - CPU: ${temp}C" >> "$LOG"
}
TEMP=$(get_temp)
log_temp "$TEMP"
echo "Температура CPU: ${TEMP}°C"
# Сравниваем через bc (bc умеет float)
if (( $(echo "$TEMP > $THRESHOLD" | bc -l) )); then
echo "ПРЕДУПРЕЖДЕНИЕ: перегрев ${TEMP}°C > ${THRESHOLD}°C"
# Здесь можно: отправить уведомление, включить вентилятор GPIO
fi
</code></pre>
<p>Запускай через cron каждые 5 минут:</p>
<pre><code class="language-bash">
# crontab -e
*/5 * * * * /home/pi/scripts/check_temp.sh >> /var/log/temp_monitor.log 2>&1
</code></pre>
<h3>Автозапуск скрипта при загрузке RPi</h3>
<p>Три способа - от простого к правильному.</p>
<p>Способ 1 - cron @reboot. Просто, работает:</p>
<pre><code class="language-bash">
crontab -e
# Добавить:
@reboot /home/pi/scripts/gpio_init.sh >> /var/log/gpio_init.log 2>&1
</code></pre>
<p>Минус: нет зависимостей, нет проверки завершения, нет удобных логов.</p>
<p>Способ 2 - systemd service. Правильно для серьёзных задач:</p>
<pre><code class="language-text">
# /etc/systemd/system/gpio-init.service
[Unit]
Description=GPIO Initialization
After=multi-user.target
[Service]
Type=oneshot
ExecStart=/home/pi/scripts/gpio_init.sh
User=pi
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
</code></pre>
<pre><code class="language-bash">
sudo systemctl daemon-reload
sudo systemctl enable gpio-init.service
sudo systemctl start gpio-init.service
# Статус
sudo systemctl status gpio-init.service
# Логи
journalctl -u gpio-init.service
</code></pre>
<p>Способ 3 - /etc/rc.local. Старый, но работает на всех дистрибутивах:</p>
<pre><code class="language-bash">
sudo nano /etc/rc.local
# Добавить перед строкой "exit 0":
/home/pi/scripts/gpio_init.sh &
</code></pre>
<h2>Практический пример: скрипт резервного копирования</h2>
<p>Теория - хорошо. Но лучше посмотреть как всё это собирается в рабочий скрипт. Скрипт бэкапа базы данных - классика которая пригодится любому.</p>
<pre><code class="language-bash">
#!/bin/bash
set -euo pipefail
# ============================================================
# backup_db.sh - бэкап PostgreSQL базы данных
# Использование: ./backup_db.sh <имя_базы> [директория_бэкапа]
# ============================================================
# Конфигурация
DB_NAME="${1:-myapp}"
BACKUP_DIR="${2:-/backup/postgres}"
KEEP_DAYS=7
LOG_FILE="/var/log/backup_db.log"
DATE=$(date +%Y-%m-%d_%H-%M-%S)
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${DATE}.sql.gz"
# Логирование
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [$1] $2" | tee -a "$LOG_FILE"
}
# Проверка зависимостей
check_deps() {
for cmd in pg_dump gzip; do
if ! command -v "$cmd" > /dev/null 2>&1; then
log "ERROR" "Не найдена команда: $cmd"
exit 1
fi
done
}
# Создание директории если не существует
prepare_dir() {
if [ ! -d "$BACKUP_DIR" ]; then
mkdir -p "$BACKUP_DIR"
log "INFO" "Создана директория: $BACKUP_DIR"
fi
}
# Бэкап
do_backup() {
log "INFO" "Начинаем бэкап базы $DB_NAME"
pg_dump "$DB_NAME" | gzip > "$BACKUP_FILE"
local size
size=$(du -h "$BACKUP_FILE" | cut -f1)
log "INFO" "Бэкап создан: $BACKUP_FILE ($size)"
}
# Удаление старых бэкапов
cleanup_old() {
log "INFO" "Удаляем бэкапы старше $KEEP_DAYS дней"
find "$BACKUP_DIR" -name "${DB_NAME}_*.sql.gz" -mtime +"$KEEP_DAYS" -delete
log "INFO" "Очистка завершена"
}
# Основная логика
check_deps
prepare_dir
do_backup
cleanup_old
log "INFO" "Готово"
</code></pre>
<p>Что здесь важно.</p>
<p><code>set -euo pipefail</code> в начале - скрипт остановится если <code>pg_dump</code> или <code>gzip</code> завершится с ошибкой. Без этого сломанный архив тихо запишется в файл.</p>
<p><code>command -v "$cmd" > /dev/null</code> - правильный способ проверить доступность команды. Лучше чем <code>which</code>: работает для встроенных команд bash, не зависит от наличия which в системе.</p>
<p><code>"${1:-myapp}"</code> - значение по умолчанию. Если первый аргумент не передан - используется <code>myapp</code>. Избавляет от ручной проверки <code>if [ -z "$1" ]</code>.</p>
<p>Функции - удобно и читаемо. Логика разбита на части. Каждую функцию можно тестировать отдельно.</p>
<h3>Скрипт мониторинга дискового пространства</h3>
<pre><code class="language-bash">
#!/bin/bash
set -euo pipefail
THRESHOLD=85
HOSTNAME=$(hostname)
LOG="/var/log/disk_monitor.log"
check_disk() {
local mount_point="$1"
local usage
# df возвращает % без знака %
usage=$(df "$mount_point" | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
echo "$(date '+%Y-%m-%d %H:%M:%S') $mount_point: ${usage}%" >> "$LOG"
if [ "$usage" -gt "$THRESHOLD" ]; then
echo "ALERT: $HOSTNAME - $mount_point заполнен на ${usage}% (порог: ${THRESHOLD}%)"
# Сюда: отправка email, telegram, slack уведомления
return 1
fi
return 0
}
# Проверяем несколько точек монтирования
EXIT_CODE=0
for mount in / /var /home /tmp; do
if mountpoint -q "$mount" 2>/dev/null; then
check_disk "$mount" || EXIT_CODE=1
fi
done
exit $EXIT_CODE
</code></pre>
<p>Запускай через cron каждые 10 минут:</p>
<pre><code class="language-bash">
*/10 * * * * /home/user/scripts/check_disk.sh >> /var/log/disk_monitor.log 2>&1
</code></pre>
<h2>Безопасность bash скриптов</h2>
<p>Скрипты в продакшне - это код. К нему применяются те же требования что к любому коду: не доверяй вводу, минимум привилегий, защита секретов.</p>
<h3>Не храни секреты в скрипте</h3>
<pre><code class="language-bash">
#!/bin/bash
# ПЛОХО - пароль в скрипте
pg_dump -U admin -W "mypassword123" mydb > backup.sql
# ХОРОШО - пароль из переменной окружения
pg_dump -U "${DB_USER}" mydb > backup.sql
# DB_PASSWORD передаётся через environment, не в скрипте
# ХОРОШО - пароль из файла с правильными правами
DB_PASSWORD=$(cat /etc/myapp/.db_password)
# Файл с правами 600: chmod 600 /etc/myapp/.db_password
# ХОРОШО - pgpass для PostgreSQL
# ~/.pgpass: hostname:port:database:username:password
# chmod 600 ~/.pgpass
pg_dump -U admin mydb > backup.sql
</code></pre>
<h3>Квотирование переменных с пользовательским вводом</h3>
<pre><code class="language-bash">
#!/bin/bash
# Получаем имя файла от пользователя
FILENAME="$1"
# ПЛОХО - shell injection
rm $FILENAME
# Если FILENAME="-rf / " - это катастрофа
# ХОРОШО - кавычки защищают от пробелов и спецсимволов
rm "$FILENAME"
# ЛУЧШЕ - проверять ввод перед использованием
if [[ "$FILENAME" =~ ^[a-zA-Z0-9._-]+$ ]]; then
rm "$FILENAME"
else
echo "Недопустимые символы в имени файла"
exit 1
fi
</code></pre>
<h3>Минимум привилегий</h3>
<pre><code class="language-bash">
# Не запускай скрипты от root без необходимости
# Если нужны права root для конкретной команды - используй sudo для этой команды
#!/bin/bash
# ПЛОХО - запускать весь скрипт от root
# sudo ./script.sh
# ХОРОШО - повышение привилегий только там где нужно
echo "Обрабатываем файлы..."
process_files # без root
echo "Перезапускаем сервис..."
sudo systemctl restart nginx # только здесь root
echo "Готово"
</code></pre>
<h3>Временные файлы</h3>
<pre><code class="language-bash">
#!/bin/bash
# ПЛОХО - предсказуемое имя временного файла
TMP_FILE=/tmp/script_temp.txt
# ХОРОШО - уникальное временное имя
TMP_FILE=$(mktemp /tmp/script.XXXXXX)
# Гарантированное удаление при выходе
cleanup() {
rm -f "$TMP_FILE"
}
trap cleanup EXIT
# Работаем с временным файлом
some_command > "$TMP_FILE"
process "$TMP_FILE"
</code></pre>
<p><code>trap cleanup EXIT</code> выполняет функцию cleanup при любом завершении скрипта - нормальном, по ошибке (<code>set -e</code>), по сигналу. Временный файл гарантированно удалится.</p>
<h3>Чтение состояния GPIO: кнопки и датчики</h3>
<p>GPIO работает в обе стороны - не только вывод, но и ввод. Кнопки, датчики, концевые выключатели.</p>
<pre><code class="language-bash">
#!/bin/bash
BUTTON_PIN=18
LED_PIN=17
# Настраиваем пины
gpio -g mode $BUTTON_PIN in
gpio -g mode $BUTTON_PIN up # подтягивающий резистор к VCC
gpio -g mode $LED_PIN out
echo "Нажми кнопку на пине $BUTTON_PIN..."
while true; do
# Читаем состояние кнопки
STATE=$(gpio -g read $BUTTON_PIN)
# При подтягивающем резисторе: 0 = нажата, 1 = отпущена
if [ "$STATE" -eq 0 ]; then
gpio -g write $LED_PIN 1
echo "Кнопка нажата - светодиод включён"
else
gpio -g write $LED_PIN 0
fi
sleep 0.05 # 50мс опрос
done
</code></pre>
<h3>Скрипт управления GPIO с аргументами</h3>
<p>Универсальный скрипт для управления пинами - удобно вызывать из Python или другого скрипта:</p>
<pre><code class="language-bash">
#!/bin/bash
# gpio_ctrl.sh - управление GPIO пином
# Использование: ./gpio_ctrl.sh <pin> <action> [value]
# Действия: init_out, init_in, write, read, toggle
set -euo pipefail
PIN="${1:-17}"
ACTION="${2:-read}"
VALUE="${3:-0}"
case "$ACTION" in
init_out)
gpio -g mode "$PIN" out
echo "Пин $PIN: настроен как выход"
;;
init_in)
gpio -g mode "$PIN" in
gpio -g mode "$PIN" up
echo "Пин $PIN: настроен как вход с подтяжкой"
;;
write)
gpio -g write "$PIN" "$VALUE"
echo "Пин $PIN: записано $VALUE"
;;
read)
STATE=$(gpio -g read "$PIN")
echo "$STATE"
;;
toggle)
CURRENT=$(gpio -g read "$PIN")
NEW=$(( 1 - CURRENT ))
gpio -g write "$PIN" "$NEW"
echo "Пин $PIN: $CURRENT -> $NEW"
;;
*)
echo "Неизвестное действие: $ACTION"
echo "Действия: init_out, init_in, write, read, toggle"
exit 1
;;
esac
</code></pre>
<p>Вызов из другого скрипта или Python:</p>
<pre><code class="language-bash">
# Включить реле на пине 17
./gpio_ctrl.sh 17 write 1
# Прочитать состояние кнопки
STATE=$(./gpio_ctrl.sh 18 read)
echo "Состояние кнопки: $STATE"
</code></pre>
<pre><code class="language-python">
import subprocess
def gpio_write(pin, value):
subprocess.run(
["bash", "/home/pi/scripts/gpio_ctrl.sh", str(pin), "write", str(value)],
check=True
)
def gpio_read(pin):
result = subprocess.run(
["bash", "/home/pi/scripts/gpio_ctrl.sh", str(pin), "read"],
capture_output=True,
text=True,
check=True
)
return int(result.stdout.strip())
# Включить LED на пине 17
gpio_write(17, 1)
# Прочитать кнопку на пине 18
button_state = gpio_read(18)
print(f"Кнопка: {button_state}")
</code></pre>
<h2>Запуск bash скриптов в Windows</h2>
<p><a href="https://it-apteka.com/punto-switcher-i-analogi-dlja-windows-macos-i-linux/" title="Punto Switcher и аналоги для Windows, macOS и Linux" target="_blank" rel="noopener" data-wpil-monitor-id="2340">Windows - не Linux</a>, но запускать bash-скрипты там реально. Четыре способа в зависимости от задачи.</p>
<h3>WSL - полноценный Linux в Windows</h3>
<p><a href="https://it-apteka.com/1243-2/" title="Сервер активации Windows (KMS) в Docker: варианты развёртывания и интеграция с Active Directory" target="_blank" rel="noopener" data-wpil-monitor-id="2341">Windows Subsystem for Linux - лучший вариант</a> для серьёзной работы. Полноценное ядро Linux, apt, все системные утилиты. Устанавливается из <a class="wpil_keyword_link" href="https://it-apteka.com/tag/powershell/" target="_blank" rel="noopener" title="PowerShell" data-wpil-keyword-link="linked" data-wpil-monitor-id="2329">PowerShell</a> от имени администратора:</p>
<pre><code class="language-powershell">
wsl --install
</code></pre>
<p>По умолчанию установится Ubuntu. После перезагрузки создай пользователя и готово.</p>
<p>Запуск bash скрипта через WSL:</p>
<pre><code class="language-bash">
# Запустить bash напрямую
wsl bash /home/user/script.sh
# Запустить скрипт Windows-пути
wsl bash "//c/Users/User/scripts/script.sh"
# Из PowerShell, получить вывод в переменную
$output = wsl bash /home/user/script.sh
Write-Host $output
</code></pre>
<p>Скрипты с Linux-путями работают нативно. Для Windows-путей нужно конвертировать: <code>C:\Scripts\</code> становится <code>//c/Scripts/</code> или <code>/mnt/c/Scripts/</code> внутри WSL.</p>
<h3>Git Bash - для разработчиков</h3>
<p>Устанавливается вместе с Git for Windows. Не нужно включать WSL, работает из коробки. Минус: нет полноценного apt, системные команды Linux отсутствуют.</p>
<p>Скачай Git for Windows: https://git-scm.com/download/win</p>
<pre><code class="language-bash">
# В терминале Git Bash
./script.sh
bash script.sh
# Запустить из CMD или PowerShell
"C:\Program Files\Git\bin\bash.exe" C:\Scripts\script.sh
</code></pre>
<p>Хороший выбор для переносимых скриптов которые не используют Linux-специфичные команды (<code>systemctl</code>, <code>/proc</code>, и т.д.).</p>
<h3>Нюансы запуска bash в WSL: пути и права</h3>
<p>WSL запускает настоящее Linux-ядро, большинство скриптов работают без изменений. Но есть нюансы.</p>
<p>Пути к файлам. Диск C: в WSL доступен как /mnt/c/. Скрипт который работает с Windows-файлами должен использовать этот путь:</p>
<pre><code class="language-bash">
# Файл C:\Users\User\Documents\data.csv доступен в WSL как:
cat /mnt/c/Users/User/Documents/data.csv
</code></pre>
<p>Права на файлы. NTFS не поддерживает Unix-права в полной мере. Файлы в /mnt/c/ часто имеют права 777. chmod на них работает непредсказуемо. Для скриптов которые должны быть исполняемыми - держи их в файловой системе WSL (/home/user/), не в Windows-разделе.</p>
<p>Производительность. Файловые операции в /mnt/c/ значительно медленнее чем в Linux-части WSL (/home/user/). Если скрипт обрабатывает много файлов - работай с копией в /home/.</p>
<h3>Cygwin - альтернатива WSL</h3>
<p>POSIX-совместимая среда для Windows. Старше WSL, иногда нужен для специфических задач совместимости. Установи с cygwin.com, запускай скрипты как в Linux.</p>
<p>Ограничение: Cygwin не использует настоящее ядро Linux - это эмуляция POSIX API. Скрипты работают, но производительность ниже WSL и бинарники Linux там не запустишь.</p>
<h3>Конвертация CRLF в LF</h3>
<p>Создал скрипт в <a href="https://it-apteka.com/ustanovka-i-nastrojka-kerio-control-poshagovoe-rukovodstvo-ot-nulja-do-rabochego-shljuza/" target="_blank" rel="noopener" data-wpil-monitor-id="2416">Windows - получил ошибку при запуске</a> в Linux:</p>
<pre><code class="language-bash">
/bin/bash^M: bad interpreter
</code></pre>
<p><code>^M</code> - это символ возврата каретки CR (ASCII 13). Windows использует CRLF (<code>\r\n</code>) как конец строки, Linux - LF (<code>\n</code>). Shebang с <code>\r</code> на конце превращается в неправильный путь.</p>
<p>Решение:</p>
<pre><code class="language-bash">
# Утилита dos2unix
sudo apt install dos2unix
dos2unix script.sh
# Или через sed
sed -i 's/\r//' script.sh
# Или через vim
vim script.sh
# :set ff=unix
# :wq
</code></pre>
<p>Настрой редактор чтобы сразу сохранять в Unix-формате. В VS Code - правый нижний угол, там показан текущий формат строк (CRLF или LF). Кликни и измени.</p>
<h2>Типичные ошибки при запуске bash скрипта</h2>
<h3>Permission denied</h3>
<pre><code class="language-bash">
bash: ./script.sh: Permission denied
</code></pre>
<p>Причина: на файле нет флага исполняемости.</p>
<p>Решение:</p>
<pre><code class="language-bash">
chmod +x script.sh
./script.sh
# Или без chmod
bash script.sh
</code></pre>
<p>Дополнительный случай: скрипт на примонтированном диске с опцией <code>noexec</code>. Тогда даже <code>chmod +x</code> не поможет. Проверь:</p>
<pre><code class="language-bash">
mount | grep "noexec"
</code></pre>
<p>Если скрипт на примонтированном разделе с noexec - запускай через явный вызов bash:</p>
<pre><code class="language-bash">
bash /mnt/backup/script.sh
</code></pre>
<h3>Command not found</h3>
<pre><code class="language-bash">
./script.sh: line 5: python3: command not found
</code></pre>
<p>Причина 1: программа не установлена. Проверь:</p>
<pre><code class="language-bash">
which python3
# если пустой вывод - не установлена
</code></pre>
<p>Причина 2: команда не в PATH (часто при запуске из cron). Проверь где находится программа и используй полный путь:</p>
<pre><code class="language-bash">
which python3
# /usr/bin/python3
# В скрипте пиши полный путь:
/usr/bin/python3 /home/user/app/main.py
</code></pre>
<h3>Скрипт не находит файлы: проблема рабочей директории</h3>
<pre><code class="language-bash">
cat: config.txt: No such file or directory
</code></pre>
<p>Причина: скрипт запущен не из своей директории. <code>config.txt</code> ищется в <a href="https://it-apteka.com/rezervnoe-kopirovanie-mikrotik-routeros-7-v-telegram-avtomatizacija-za-20-minut/" title="Резервное копирование MikroTik RouterOS 7 в Telegram: рабочий скрипт и разбор ошибок" target="_blank" rel="noopener" data-wpil-monitor-id="2342">рабочей директории откуда запустили скрипт</a>, а не там где лежит скрипт.</p>
<p>Решение - в начале скрипта перейти в его директорию:</p>
<pre><code class="language-bash">
#!/bin/bash
# Определить директорию скрипта и перейти в неё
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Теперь относительные пути работают от директории скрипта
cat config.txt # найдёт файл рядом со скриптом
python3 helper.py # запустит скрипт из той же директории
</code></pre>
<p><code>${BASH_SOURCE[0]}</code> - путь к текущему скрипту. Работает при прямом запуске, через source, через bash. Надёжнее чем <code>$0</code>.</p>
<h3>Скрипт не останавливается при ошибке</h3>
<p>По умолчанию bash продолжает выполнение даже если команда вернула ошибку. Это поведение по умолчанию приводит к катастрофическим последствиям в деструктивных операциях - скрипт "удалил" директорию, получил ошибку, пошёл дальше и сделал что-то непоправимое.</p>
<pre><code class="language-bash">
#!/bin/bash
# Добавь в начало каждого продакшн-скрипта
set -e # завершить скрипт при любой ошибке
set -u # завершить при использовании неопределённой переменной
set -o pipefail # код ошибки pipeline = код последней упавшей команды
# Кратко:
set -euo pipefail
</code></pre>
<p>Что каждый флаг делает:</p>
<p><code>set -e</code>: если команда вернула ненулевой код - скрипт завершается. Исключения: команды в <code>if</code>, после <code>||</code>, в условии <code>while</code>.</p>
<p><code>set -u</code>: использование необъявленной переменной - ошибка. Защищает от опечаток в именах переменных. Без этого <code>$USRE</code> вместо <code>$USER</code> тихо подставит пустую строку.</p>
<p><code>set -o pipefail</code>: в конвейере <code>cmd1 | cmd2</code> код возврата - код последней команды. Без pipefail если <code>cmd1</code> упала, а <code>cmd2</code> отработала нормально - конвейер считается успешным. С pipefail - нет.</p>
<h3>Переменные с пробелами</h3>
<pre><code class="language-bash">
# Проблема
DIR=/home/user/my folder # директория с пробелом
cd $DIR # bash воспримет это как: cd /home/user/my folder (два аргумента)
# Решение: всегда кавычки вокруг переменных
cd "$DIR"
# То же с массивами и глобами
FILES=*.log
rm $FILES # может не сработать как ожидается
rm "$FILES" # кавычки сохранят шаблон
</code></pre>
<p>Правило: переменные в bash всегда в двойных кавычках если не уверен что там нет пробелов. <code>"$VAR"</code> вместо <code>$VAR</code>. Исключение - числовые переменные в арифметических выражениях.</p>
<h3>Heredoc и CRLF в cron</h3>
<p>Heredoc - удобная конструкция для многострочного ввода. Частая ловушка: если файл скрипта с heredoc создан на Windows, CRLF в heredoc приводит к неожиданным результатам:</p>
<pre><code class="language-bash">
# Правильный heredoc
cat > /tmp/config.txt << 'EOF'
server = localhost
port = 5432
EOF
# Если скрипт в CRLF формате - heredoc будет содержать \r в конце строк
# Решение: dos2unix на файл скрипта
dos2unix script.sh
</code></pre>
<h2>Bash в связке с другими инструментами</h2>
<p>Bash редко живёт сам по себе. В реальных задачах он работает в связке.</p>
<h3>Bash + Python: правильное разделение</h3>
<p>Практическое правило: bash для последовательности операций и вызова системных утилит, Python для логики, работы с данными, API-запросов. Когда Python вызывает bash - он получает лучшее от обоих.</p>
<p>Типичный шаблон: Python собирает параметры и данные, bash выполняет системные операции:</p>
<pre><code class="language-python">
import subprocess
import json
from datetime import datetime
def deploy_service(service_name, version, config):
# Python: подготовка параметров, валидация
if not service_name.isalnum():
raise ValueError(f"Недопустимое имя сервиса: {service_name}")
deploy_params = json.dumps({
"service": service_name,
"version": version,
"timestamp": datetime.now().isoformat()
})
# Bash: системные операции
result = subprocess.run(
["bash", "/opt/scripts/deploy.sh", service_name, version],
capture_output=True,
text=True,
env={**__import__('os').environ, "DEPLOY_CONFIG": deploy_params}
)
if result.returncode != 0:
raise RuntimeError(f"Деплой упал:\n{result.stderr}")
return result.stdout
</code></pre>
<h3>Bash + systemd: правильный автозапуск</h3>
<p>Cron @reboot прост, но systemd даёт больше контроля. Для продакшн-скриптов которые должны работать постоянно или запускаться при старте - systemd.</p>
<p>Отличие от cron @reboot: systemd отслеживает процесс. Если скрипт упал - можно настроить автоматический рестарт. Cron @reboot запустил и забыл.</p>
<pre><code class="language-text">
# /etc/systemd/system/monitor.service
[Unit]
Description=Server Monitor Script
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=monitor
ExecStart=/home/monitor/scripts/monitor.sh
Restart=on-failure
RestartSec=30s
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
</code></pre>
<p><code>Restart=on-failure</code> - перезапустить если скрипт завершился с ненулевым кодом. <code>RestartSec=30s</code> - пауза перед рестартом. <code>After=network-online.target</code> - запускать только когда сеть готова. Всего этого нет в cron.</p>
<h3>Bash + Ansible: автоматизация инфраструктуры</h3>
<p>Ansible управляет конфигурацией через YAML-описания и SSH. Внутри Ansible-задач можно вызывать bash-скрипты через модуль <code>script</code> или <code>shell</code>. Это полезно для операций которые сложно описать через Ansible-модули но легко - через bash.</p>
<pre><code class="language-text">
# В Ansible playbook:
- name: Запустить кастомный скрипт конфигурации
script: /local/path/setup.sh arg1 arg2
args:
executable: /bin/bash
</code></pre>
<p>Это не замена нативным Ansible-модулям - они идемпотентны, a скрипты - нет. Но для миграций, первоначальной настройки и операций без готового модуля - работает.</p>
<h2>Диагностика за 2 минуты: скрипт не работает</h2>
<p>Скрипт упал. Пять шагов от симптома к причине.</p>
<p>Шаг 1: Запусти вручную и посмотри вывод. Звучит очевидно - но большинство "таинственных" ошибок обнаруживается при первом же ручном запуске в терминале.</p>
<pre><code class="language-bash">
bash script.sh
echo "Код выхода: $?"
</code></pre>
<p>Шаг 2: Включи трассировку.</p>
<pre><code class="language-bash">
bash -x script.sh 2>&1 | head -50
</code></pre>
<p>Смотришь на вывод и видишь: на какой команде остановился, с какими подставленными значениями переменных.</p>
<p>Шаг 3: Проверь окружение как у cron.</p>
<pre><code class="language-bash">
env -i PATH=/usr/bin:/bin HOME=/root bash script.sh
</code></pre>
<p>Если скрипт в этом окружении не работает - проблема в PATH или переменных окружения.</p>
<p>Шаг 4: Проверь синтаксис и распространённые ошибки.</p>
<pre><code class="language-bash">
bash -n script.sh
shellcheck script.sh
</code></pre>
<p>Шаг 5: Смотри системные логи.</p>
<pre><code class="language-bash">
# Cron логи
grep CRON /var/log/syslog | grep "script.sh" | tail -10
# Systemd логи
journalctl -u myscript.service --since "1 hour ago" --no-pager
</code></pre>
<p>В большинстве случаев проблема находится на шагах 1-3. Шаги 4-5 нужны для более хитрых ситуаций.</p>
<h2>Полезные паттерны bash: что пригодится в реальных скриптах</h2>
<p>Несколько конструкций которые встречаются постоянно. Выучи однажды - используй везде.</p>
<h3>Проверка что скрипт запущен от root</h3>
<pre><code class="language-bash">
#!/bin/bash
if [ "$EUID" -ne 0 ]; then
echo "Этот скрипт нужно запускать от root"
echo "Используй: sudo $0"
exit 1
fi
</code></pre>
<p><code>$EUID</code> - эффективный User ID текущего процесса. 0 = root. Проверяй это в начале скрипта который требует привилегий - иначе скрипт молча упадёт на первой привилегированной команде.</p>
<h3>Spinner и прогресс для долгих операций</h3>
<pre><code class="language-bash">
#!/bin/bash
spinner() {
local pid=$1
local delay=0.1
local spinstr='|/-\'
while kill -0 "$pid" 2>/dev/null; do
local temp=${spinstr#?}
printf " [%c] " "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b\b\b\b"
done
printf " \b\b\b\b"
}
echo -n "Копирование файлов..."
cp -r /source /destination &
spinner $!
echo " готово"
</code></pre>
<h3>Параллельное выполнение с ограничением числа процессов</h3>
<pre><code class="language-bash">
#!/bin/bash
MAX_JOBS=4
PIDS=()
process_file() {
local file="$1"
# Обработка файла
sleep 1
echo "Обработан: $file"
}
for file in /data/*.csv; do
process_file "$file" &
PIDS+=($!)
# Ждём если запущено слишком много процессов
while [ ${#PIDS[@]} -ge $MAX_JOBS ]; do
for i in "${!PIDS[@]}"; do
if ! kill -0 "${PIDS[$i]}" 2>/dev/null; then
unset 'PIDS[$i]'
fi
done
PIDS=("${PIDS[@]}") # пересоздаём без пустых элементов
sleep 0.1
done
done
# Ждём завершения всех
wait
echo "Все файлы обработаны"
</code></pre>
<h3>Mutex: защита от параллельного запуска</h3>
<p>Когда скрипт запускается по cron и ты хочешь убедиться что не запущены два экземпляра одновременно:</p>
<pre><code class="language-bash">
#!/bin/bash
LOCK_FILE="/var/run/myscript.lock"
# Проверяем и создаём lock-файл атомарно
if ! mkdir "$LOCK_FILE" 2>/dev/null; then
PID=$(cat "$LOCK_FILE/pid" 2>/dev/null)
echo "Скрипт уже запущен (PID: $PID)"
exit 1
fi
# Сохраняем PID
echo $$ > "$LOCK_FILE/pid"
# Удаляем lock при выходе
cleanup() {
rm -rf "$LOCK_FILE"
}
trap cleanup EXIT
echo "Скрипт запущен, PID: $$"
# Основная логика скрипта
sleep 10
echo "Работа завершена"
</code></pre>
<p><code>mkdir</code> - атомарная операция: либо создал директорию, либо нет. Используется как mutex. Надёжнее чем проверка через <code>[ -f lock ]</code> + создание файла - между двумя операциями может вклиниться второй процесс.</p>
<h3>Retry: повторить при ошибке</h3>
<pre><code class="language-bash">
#!/bin/bash
retry() {
local max_attempts="$1"
local delay="$2"
shift 2
local cmd="$@"
local attempt=1
while [ $attempt -le $max_attempts ]; do
echo "Попытка $attempt/$max_attempts: $cmd"
if eval "$cmd"; then
return 0
fi
if [ $attempt -lt $max_attempts ]; then
echo "Неудача. Повтор через ${delay}с..."
sleep "$delay"
fi
attempt=$((attempt + 1))
done
echo "Все $max_attempts попыток исчерпаны"
return 1
}
# Использование: до 3 попыток с паузой 5 секунд
retry 3 5 curl -f https://api.example.com/health
retry 3 10 pg_dump mydb > /backup/mydb.sql
</code></pre>
<p>Полезно для сетевых операций, для команд которые могут упасть из-за временной недоступности ресурса. Не надо усложнять скрипт вручной логикой - оберни команду в retry.</p>
<h2>Отладка bash скриптов</h2>
<p>Есть несколько инструментов для диагностики.</p>
<h3>bash -x: трассировка выполнения</h3>
<pre><code class="language-bash">
bash -x script.sh
</code></pre>
<p>Bash выводит каждую команду перед выполнением, со знаком <code>+</code>. Переменные подставлены. Видишь что именно выполняется и с какими значениями.</p>
<p>Включить трассировку только для части скрипта:</p>
<pre><code class="language-bash">
#!/bin/bash
echo "Начало"
set -x # включить трассировку
RESULT=$(some_complex_command)
echo "Результат: $RESULT"
set +x # выключить трассировку
echo "Конец"
</code></pre>
<h3>bash -n: проверка синтаксиса</h3>
<pre><code class="language-bash">
bash -n script.sh
</code></pre>
<p>Проверяет синтаксис без выполнения. Находит незакрытые кавычки, неправильные условия, пропущенные <code>fi</code> и <code>done</code>.</p>
<h3>shellcheck: статический анализатор</h3>
<pre><code class="language-bash">
# Установка
sudo apt install shellcheck
# Проверить скрипт
shellcheck script.sh
</code></pre>
<p>shellcheck - незаменимый инструмент. Находит не только синтаксические ошибки, но и логические: незакавыченные переменные, неправильное сравнение чисел, устаревшие конструкции. Если пишешь скрипты регулярно - поставь shellcheck.</p>
<h3>Логирование в скрипте</h3>
<pre><code class="language-bash">
#!/bin/bash
LOG="/var/log/myscript.log"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [$1] $2" | tee -a "$LOG"
}
log "INFO" "Скрипт запущен"
log "INFO" "Обрабатываем файлы..."
# Что-то делаем
if some_command; then
log "INFO" "Команда выполнена успешно"
else
log "ERROR" "Команда завершилась с ошибкой"
exit 1
fi
log "INFO" "Скрипт завершён"
</code></pre>
<p><code>tee -a "$LOG"</code> одновременно пишет в файл и выводит в терминал. Удобно для скриптов которые запускаются и вручную, и через cron.</p>
<h2>Шпаргалка: запуск bash скрипта</h2>
<table>
<thead>
<tr>
<th>Задача</th>
<th>Команда</th>
</tr>
</thead>
<tbody>
<tr>
<td>Дать права на выполнение</td>
<td>chmod +x script.sh</td>
</tr>
<tr>
<td>Запустить из текущей папки</td>
<td>./script.sh</td>
</tr>
<tr>
<td>Запустить без chmod</td>
<td>bash script.sh</td>
</tr>
<tr>
<td>Запустить по полному пути</td>
<td>/home/user/scripts/script.sh</td>
</tr>
<tr>
<td>Проверить синтаксис</td>
<td>bash -n script.sh</td>
</tr>
<tr>
<td>Трассировка выполнения</td>
<td>bash -x script.sh</td>
</tr>
<tr>
<td>Редактировать cron</td>
<td>crontab -e</td>
</tr>
<tr>
<td>Посмотреть cron задачи</td>
<td>crontab -l</td>
</tr>
<tr>
<td>Запустить из Python</td>
<td>subprocess.run(["bash", "script.sh"])</td>
</tr>
<tr>
<td>Запустить в Windows WSL</td>
<td>wsl bash script.sh</td>
</tr>
<tr>
<td>Конвертировать CRLF в LF</td>
<td>dos2unix script.sh</td>
</tr>
<tr>
<td>Статический анализ</td>
<td>shellcheck script.sh</td>
</tr>
</tbody>
</table>
<h2>FAQ</h2>
<h3>Почему bash скрипт не запускается через cron хотя вручную работает?</h3>
<p>Почти всегда - проблема с PATH или с путями к файлам. Cron запускает задачи с минимальным PATH: только <code>/usr/bin:/bin</code>. Команды которые у тебя есть в интерактивной сессии - node, python3, docker - могут быть не найдены.</p>
<p>Решение: используй полные пути к исполняемым файлам (<code>/usr/bin/python3</code> вместо <code>python3</code>) и полные пути к скриптам (<code>/home/user/scripts/backup.sh</code> вместо <code>./backup.sh</code>). Обязательно добавь вывод в лог: <code>>> /var/log/script.log 2>&1</code> - иначе ошибки уходят в никуда.</p>
<h3>Как запустить bash скрипт с аргументами?</h3>
<p>Аргументы передаются после имени скрипта через пробел, внутри скрипта доступны как <code>$1</code>, <code>$2</code>, и т.д.:</p>
<pre><code class="language-bash">
# Запуск с аргументами
./backup.sh mydb /backup/path
# Внутри скрипта
#!/bin/bash
DB_NAME=$1 # mydb
DEST_PATH=$2 # /backup/path
echo "Бэкап $DB_NAME в $DEST_PATH"
</code></pre>
<p><code>$0</code> - имя самого скрипта. <code>$#</code> - количество аргументов. <code>$@</code> - все аргументы как массив.</p>
<h3>Как запустить bash скрипт при загрузке Linux?</h3>
<p>Три способа в порядке предпочтения: systemd service (правильно, гибко, логи через journald), cron @reboot (просто, работает везде), /etc/rc.local (совместимость со старыми системами). Для RPi или домашнего сервера проще всего cron @reboot - добавь <code>@reboot /полный/путь/script.sh</code> в crontab.</p>
<h3>Как проверить что bash скрипт завершился с ошибкой?</h3>
<p>После выполнения любой команды или скрипта код возврата хранится в <code>$?</code>:</p>
<pre><code class="language-bash">
bash script.sh
echo "Код возврата: $?"
# 0 = успех, не-0 = ошибка
</code></pre>
<p>Из Python: <code>result.returncode</code> у subprocess. Нулевой - успех, любой другой - ошибка. Добавь <code>set -e</code> в скрипт чтобы он сам останавливался при первой ошибке.</p>
<h3>Зачем нужен shebang и что будет без него?</h3>
<p>Shebang (<code>#!/bin/bash</code>) говорит операционной системе каким интерпретатором запускать файл. Без shebang при запуске <code>./script.sh</code> система не знает что делать с файлом. Поведение зависит от системы: может запустить через /bin/sh (не bash, другой синтаксис), может выдать ошибку. При явном вызове <code>bash script.sh</code> shebang не нужен - ты сам указываешь интерпретатор. Но shebang лучше всегда писать: это документирует намерение и защищает от неожиданностей.</p>
<h3>Чем subprocess.run отличается от os.system в Python?</h3>
<p><code>os.system</code> запускает команду через системный shell и возвращает только код возврата. Вывод идёт прямо в терминал - перехватить нельзя. <code>subprocess.run</code> гибче: можно перехватить stdout и stderr, передавать аргументы без shell injection, задавать таймаут, получать структурированный результат. Для новых проектов - всегда subprocess. <code>os.system</code> оставь для однострочников в интерактивном режиме.</p>
<h3>Как передать переменные окружения в bash скрипт?</h3>
<p>Три способа. Прямо перед командой - для одного вызова:</p>
<pre><code class="language-bash">
DB_HOST=localhost DB_PORT=5432 ./script.sh
</code></pre>
<p>Через export - для текущей сессии и дочерних процессов:</p>
<pre><code class="language-bash">
export DB_HOST=localhost
export DB_PORT=5432
./script.sh
</code></pre>
<p>Через .env файл с source:</p>
<pre><code class="language-bash">
# .env файл
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
# В скрипте или в терминале
source /path/to/.env
</code></pre>
<p>В cron переменные задаются прямо в crontab до первой задачи:</p>
<pre><code class="language-bash">
DB_HOST=localhost
DB_PORT=5432
0 3 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1
</code></pre>
<h3>Как сделать bash скрипт интерактивным?</h3>
<pre><code class="language-bash">
#!/bin/bash
# Запросить строку
read -p "Введите имя базы данных: " DB_NAME
echo "База: $DB_NAME"
# Запросить пароль (без эха)
read -sp "Введите пароль: " PASSWORD
echo ""
# С таймаутом - 5 секунд
read -t 5 -p "Продолжить? (y/N) " CONFIRM || true
if [[ "$CONFIRM" =~ ^[Yy]$ ]]; then
echo "Продолжаем"
else
echo "Отменено"
exit 0
fi
</code></pre>
<h3>Как отловить ошибку в bash скрипте и выполнить действие?</h3>
<p>Через trap на ERR - выполнит функцию при любой ошибке:</p>
<pre><code class="language-bash">
#!/bin/bash
set -euo pipefail
error_handler() {
local exit_code=$?
local line_number=$1
echo "Ошибка на строке $line_number (код: $exit_code)"
}
trap 'error_handler $LINENO' ERR
echo "Шаг 1"
false # ошибка - вызовется error_handler
echo "Этот вывод не появится"
</code></pre>
<p>Если команда упадёт - вызовется error_handler с номером строки, и только потом скрипт завершится.</p>
<h2>Архитектура: как скрипт вписывается в систему</h2>
<p>Bash скрипт - это не изолированная программа. Он всегда часть какой-то системы: запускается планировщиком, вызывается другой программой, управляет сервисами. Схема показывает типичные сценарии.</p>
<pre class="mermaid">%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
A["Cron / systemd timer"] --> B["bash script.sh"]
C["Python subprocess.run"] --> B
D["Ручной запуск терминал"] --> B
E["@reboot автозапуск"] --> B
B --> F["Системные утилиты nginx mysql pg_dump"]
B --> G["Файловая система логи бэкапы"]
B --> H["Сервисы systemctl start stop"]
B --> I["GPIO Raspberry Pi"]
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style C fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style D fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style E fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style B fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style F fill:#f8fafc,stroke:#94a3b8,stroke-width:2px,color:#1e293b
style G fill:#f8fafc,stroke:#94a3b8,stroke-width:2px,color:#1e293b
style H fill:#f8fafc,stroke:#94a3b8,stroke-width:2px,color:#1e293b
style I fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#c2410c
</pre>
<p>В центре - bash скрипт. Его вызывают сверху: cron по расписанию, Python когда нужны системные операции, ты сам из терминала. Снизу скрипт управляет: системными утилитами, файлами, сервисами. На Raspberry Pi - ещё и GPIO.</p>
<p>Задача скрипта - склеить эти части в нужной последовательности. Bash не заменяет ни cron, ни Python, ни systemd. Он между ними - как слой клея.</p>
<h3>Что лучше: bash скрипт или Python для автоматизации?</h3>
<p>Зависит от задачи. Bash хорош когда нужно: запустить несколько команд в последовательности, поработать с файлами через find/grep/awk, вызвать системные утилиты, написать быстро и просто. Python хорош когда нужно: обработать данные, работать с API, сложная логика с условиями и циклами, обработка ошибок с разными ветками.</p>
<p>На практике они дополняют друг друга. Python вызывает bash когда нужны системные операции. Bash вызывает Python когда нужна логика. Не надо выбирать - надо знать оба.</p>
<h3>Как посмотреть что происходит в скрипте который уже запущен?</h3>
<p>Если скрипт пишет в лог - смотри лог в реальном времени:</p>
<pre><code class="language-bash">
tail -f /var/log/myscript.log
</code></pre>
<p>Найти PID запущенного скрипта:</p>
<pre><code class="language-bash">
pgrep -f "script.sh"
ps aux | grep "script.sh"
</code></pre>
<p>Посмотреть что делает процесс прямо сейчас:</p>
<pre><code class="language-bash">
strace -p PID 2>&1 | head -30
</code></pre>
<p><code>strace</code> показывает системные вызовы процесса - куда он обращается, что читает и пишет. Для диагностики зависших скриптов - незаменимо.</p>
<h2>Итог</h2>
<p>Bash <a href="https://it-apteka.com/komandlety-powershell-10-komand/" title="Командлеты PowerShell: 10 команд которые ты будешь использовать каждый день" target="_blank" rel="noopener" data-wpil-monitor-id="2618">скрипт запускается тремя командами</a>: chmod +x, ./script.sh, и готово. Всё остальное - детали конкретных сценариев.</p>
<p>Запомни три вещи которые снимут большинство проблем. В cron - полные пути и вывод в лог. В скриптах - set -euo pipefail в начале чтобы скрипт не продолжал на сломанном стеке. В Python - subprocess.run с capture_output=True чтобы видеть что происходит.</p>
<p>Инструменты которые стоит поставить сразу: shellcheck для статического анализа скриптов (находит проблемы до запуска), dos2unix для конвертации <a href="https://it-apteka.com/mnogopotochnoe-kopirovanie-fajlov-v-windows-uskorjaem-v-razy/" title="Многопоточное копирование файлов Windows: Robocopy, FastCopy, TeraCopy" target="_blank" rel="noopener" data-wpil-monitor-id="2503">файлов от Windows</a>. Оба есть в apt.</p>
<p>Скрипты пишешь руками. Первые три будут с ошибками. Четвёртый уже будет работать с первого запуска. На двадцатом ловишь себя на том что добавляешь set -euo pipefail автоматически, не задумываясь. Это и есть мышечная память сетевого инженера.</p>
"Что-то
<br />
Напиши в комментарии что именно не работает: ОС, команда запуска, текст ошибки. Разберёмся.<br />
Быстрый ответ: как запустить bash скрипт
- Дай права: chmod +x script.sh
- Запусти из текущей папки: ./script.sh
- Или без chmod: bash script.sh
- В cron: всегда полные пути, вывод в лог: 0 3 * * * /полный/путь/script.sh >> /var/log/script.log 2>&1
- Из Python: subprocess.run([«bash», «script.sh»])
- В Windows через WSL: wsl bash script.sh
Знакомо?
Пишешь скрипт. В терминале работает идеально. Добавляешь в cron — тишина. Ни ошибки, ни вывода. Скрипт как будто и не запускался.
Или запускаешь через Python — command not found. Хотя та же команда в терминале работает.
Это не магия и не баг bash. Это три классических грабли на которые наступают все: отсутствие прав на выполнение, другое окружение в cron, относительные пути которые ломаются когда меняется рабочая директория.
В этой статье разберём запуск bash скрипта по всем сценариям: Linux, cron, Python, Windows, Raspberry Pi. Плюс troubleshooting по каждой типичной ошибке — с командами, не с советами «попробуй перезагрузиться.
Что понадобится:
Три проблемы которые bash скрипт решает за одно утро
Прежде чем переходить к командам — посмотрим на задачи. Потому что «как запустить bash скрипт» отвечает не один, а несколько разных вопросов в зависимости от ситуации.
Ситуация первая: у тебя есть набор команд которые ты запускаешь каждый раз руками. Сначала это одна-две команды, потом три, потом пять, потом ты ошибаешься в третьей и переделываешь с нуля. Bash скрипт — это сохранённая последовательность. Один раз написал правильно, больше не ошибаешься.
Ситуация вторая: нужно запускать что-то по расписанию. Бэкап ночью, очистка логов в воскресенье, проверка состояния каждые 10 минут. Руками — забудешь. Cron плюс bash — не забудет. Запись в crontab занимает минуту.
Ситуация третья: Python-приложение или другая система должна вызывать системные команды. Установить пакет, перезапустить сервис, скопировать файлы. Python умеет это через subprocess — но нужно понять как это настроить правильно.
Всё это разные сценарии — и у каждого свои нюансы. В статье разберём все три, плюс Windows через WSL и Raspberry Pi GPIO.
Почему bash скрипты ломаются по-разному в разных контекстах
Один и тот же скрипт может работать в одном месте и падать в другом. Это не рандом — у каждого контекста своё окружение.
Интерактивный терминал. Ты залогинился как user. Bash загрузил ~/.bashrc и ~/.bash_profile — там твой PATH, твои алиасы, твои переменные окружения. Команды находятся потому что ~/.local/bin в PATH.
Cron. Запускает задачи с минимальным окружением: PATH=/usr/bin:/bin, и всё. Никаких ~/.bashrc. Никаких пользовательских переменных. Команды которые были в ~./local/bin — не находятся.
Python subprocess. Наследует окружение Python-процесса. Зависит от того как Python запущен: через virtualenv, системный Python, Docker-контейнер — у каждого своё окружение.
systemd. Чистое окружение, задаётся в файле unit. По умолчанию PATH стандартный системный. Пользовательские переменные — нет.
Из этого следует одно практическое правило: в скриптах предназначенных для автоматического запуска — всегда полные пути к командам и файлам. Никогда не полагайся на PATH или рабочую директорию.
Когда bash скрипт спасает: реальные сценарии
Bash скрипт начинается с усталости делать одно и то же вручную. Три типичных сценария когда он окупается за первый же запуск.
Резервное копирование. Каждый день: подключиться к серверу, выполнить pg_dump, заархивировать, переместить на хранилище, удалить старые копии. Пять шагов руками — это источник ошибок. Пять строк в bash плюс cron — это надёжная процедура которая работает пока ты спишь.
Деплой приложения. Pull из git, остановить сервис, установить зависимости, запустить миграции, поднять сервис, проверить статус. Десять команд которые нужно запомнить в правильном порядке. Скрипт делает это воспроизводимым: одна команда, один результат.
Мониторинг. Проверить что сервисы живы, что диск не переполнен, что сертификаты не истекают. Вручную — забудешь. Скрипт в cron — не забудет. Пришлёт уведомление когда что-то пойдёт не так.
Во всех трёх случаях bash работает как связующий слой между инструментами. Он не заменяет Python для сложной логики или Ansible для управления инфраструктурой. Он склеивает утилиты которые уже есть, в нужном порядке, автоматически.
Что такое bash скрипт и как он устроен
Bash скрипт — текстовый файл с командами. Теми же командами которые ты набираешь в терминале вручную — только собранными в один файл чтобы запускать их одной командой.
Никакой компиляции. Никаких бинарников. Открываешь в nano, пишешь команды, сохраняешь, запускаешь.
Минимальный рабочий скрипт — три строки:
#!/bin/bash
echo "Hello Linux"
date
Первая строка — shebang. Это не комментарий. #! плюс путь к интерпретатору — это инструкция операционной системе: «запускай этот файл вот этой программой». Без shebang система не знает что с файлом делать. Может открыть в редакторе, может выдать ошибку, может запустить через sh вместо bash — поведение непредсказуемое.
/bin/bash — путь к bash. На большинстве Linux-систем он там и живёт. Если не уверен:
which bash
Есть более переносимый вариант shebang:
#!/usr/bin/env bash
env bash ищет bash через переменную PATH, а не по жёсткому пути. Актуально для macOS, FreeBSD, и ситуаций когда bash установлен не в /bin/bash. Для простых домашних скриптов разницы нет. Для скриптов которые будут работать на разных системах — используй env bash.
Полная структура bash скрипта
Разберём скелет из которого строится любой рабочий скрипт:
#!/bin/bash
# Безопасный режим - об этом ниже
set -euo pipefail
# Переменные
NAME="Linux"
DATE=$(date +%Y-%m-%d)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Вывод
echo "Привет, $NAME!"
echo "Сегодня: $DATE"
echo "Скрипт находится в: $SCRIPT_DIR"
# Условие
if [ -f /etc/hosts ]; then
echo "Файл /etc/hosts существует"
else
echo "Файл не найден"
fi
# Цикл
for i in 1 2 3 4; do
echo "Итерация $i"
done
# Функция
greet() {
local name=$1
echo "Привет, $name!"
}
greet "мир"
Разберём что тут важно.
set -euo pipefail — три флага которые делают скрипт предсказуемым. Без них bash по умолчанию продолжает выполнение даже когда команда завершилась с ошибкой. Это нередко приводит к тому что скрипт «выполнился успешно» но половина шагов тихо упала. Подробнее в разделе про типичные ошибки.
SCRIPT_DIR — трюк для получения пути к директории скрипта. Нужен чтобы скрипт находил файлы рядом с собой независимо от того откуда его запустили. Пригодится когда будешь добавлять скрипт в cron.
$(command) — подстановка вывода команды в переменную. DATE=$(date +%Y-%m-%d) запускает команду date и записывает результат в переменную.
Запуск bash скрипта в Linux: три способа
Есть три способа запустить скрипт. Разница — в правах и в том, кто именно его выполняет.
Шаг 0: дать права на выполнение
Первое что нужно сделать с новым скриптом — дать ему права на выполнение. Это делается один раз:
chmod +x script.sh
Без этого Linux откажется запускать файл как программу — даже если внутри правильный bash-код. Это защита: не каждый текстовый файл должен быть исполняемым.
Посмотри что изменилось:
ls -la script.sh
До chmod: -rw-r--r-- — нет символа x, файл нельзя выполнить.
После chmod: -rwxr-xr-x — x появился у владельца, группы и остальных.
Разные варианты chmod:
chmod +x script.sh # права на выполнение всем
chmod u+x script.sh # только владельцу
chmod 755 script.sh # rwxr-xr-x (то же что chmod +x, числом)
chmod 700 script.sh # rwx------ (только владелец, никто больше не видит)
Для скрипта который запускает только твой пользователь — chmod 700. Для скрипта в общей директории — chmod 755.
Способ 1: прямой запуск через ./
./script.sh
Точка и слеш означают: «запустить файл из текущей директории». Нужны потому что bash ищет команды в директориях из переменной PATH — и текущая директория в PATH не входит (по соображениям безопасности). Без ./ bash скажет command not found.
Права на выполнение нужны.
Способ 2: через интерпретатор bash
bash script.sh
Явно вызываешь bash и передаёшь ему файл как аргумент. chmod +x не нужен — bash не проверяет флаг исполняемости при таком запуске. Полезно для отладки чужих скриптов и для быстрого запуска без лишних телодвижений.
Способ 3: по полному пути
/home/user/scripts/script.sh
Нужен когда скрипт запускается не из его директории: из cron, systemd, другого скрипта. Права на выполнение нужны.
Правило: в любой автоматизации — cron, systemd, Docker — всегда используй полные пути. Никогда не рассчитывай на рабочую директорию или PATH.
Быстрая проверка перед запуском
Прежде чем запускать — проверь синтаксис. Bash это умеет:
bash -n script.sh
Флаг -n означает «проверь синтаксис, не выполняй». Если ошибок нет — тишина (хорошо). Если есть — покажет строку и описание ошибки.
bash -x script.sh
Флаг -x — режим трассировки. Bash выполняет скрипт и выводит каждую команду перед выполнением. Незаменимо при отладке: видишь что именно выполняется, с какими подставленными значениями переменных.
Переменные, условия и циклы: практика без учебника
Bash-скрипт без переменных — это просто список команд. Переменные появляются как только нужно что-то переиспользовать или передать между шагами.
Переменные: основные правила
#!/bin/bash
# Объявление переменной - БЕЗ пробелов вокруг =
NAME="Андрей"
PORT=8080
LOG_DIR="/var/log/myapp"
# Использование - знак $
echo "Привет, $NAME"
echo "Порт: $PORT"
echo "Логи: $LOG_DIR"
# В фигурных скобках - когда переменная идёт вплотную к тексту
FILE_PREFIX="backup"
echo "${FILE_PREFIX}_2024.tar.gz"
# Подстановка вывода команды
CURRENT_DATE=$(date +%Y-%m-%d)
HOSTNAME=$(hostname)
FREE_DISK=$(df -h / | awk 'NR==2 {print $4}')
echo "Дата: $CURRENT_DATE"
echo "Хост: $HOSTNAME"
echo "Свободно на /: $FREE_DISK"
Пробелы вокруг = — главная ловушка для новичков. NAME = "value" это не присвоение — это команда NAME с аргументами = и "value". Bash выдаст command not found.
Специальные переменные
#!/bin/bash
# $0 - имя скрипта
# $1, $2, ... - аргументы скрипта
# $# - количество аргументов
# $@ - все аргументы как массив
# $? - код возврата последней команды
# $$ - PID текущего процесса
# $! - PID последнего фонового процесса
echo "Скрипт: $0"
echo "Аргументов: $#"
echo "Первый аргумент: $1"
echo "PID скрипта: $$"
# Проверка что передан нужный аргумент
if [ $# -lt 1 ]; then
echo "Использование: $0 "
exit 1
fi
DB_NAME=$1
echo "Работаем с базой: $DB_NAME"
Условия: if, case, &&, ||
#!/bin/bash
FILE="/etc/hosts"
PORT=80
# Проверка существования файла
if [ -f "$FILE" ]; then
echo "$FILE существует"
fi
# Проверка директории
if [ -d "/var/log" ]; then
echo "Директория /var/log есть"
fi
# Сравнение строк
STATUS="running"
if [ "$STATUS" = "running" ]; then
echo "Сервис работает"
elif [ "$STATUS" = "stopped" ]; then
echo "Сервис остановлен"
else
echo "Неизвестный статус: $STATUS"
fi
# Сравнение чисел
if [ $PORT -eq 80 ]; then
echo "Стандартный HTTP порт"
fi
if [ $PORT -gt 1024 ]; then
echo "Непривилегированный порт"
fi
# Короткая форма - && и ||
[ -f /etc/nginx/nginx.conf ] && echo "nginx конфиг найден"
[ -f /etc/nginx/nginx.conf ] || echo "nginx не установлен"
Квадратные скобки [ ] — это команда test. Пробелы внутри обязательны. [ -f file ] работает. [-f file] — нет.
Двойные скобки [[ ]] — bash-расширение. Поддерживают регулярные выражения, логические операторы без экранирования, не требуют кавычек вокруг переменных. Если пишешь только для bash (shebang #!/bin/bash) — используй [[ ]].
Циклы: for, while, until
#!/bin/bash
# for - перебор списка
for server in web1 web2 web3; do
echo "Проверяю $server..."
ping -c 1 "$server" > /dev/null 2>&1 && echo " OK" || echo " НЕДОСТУПЕН"
done
# for - перебор файлов
for log_file in /var/log/*.log; do
SIZE=$(du -h "$log_file" | cut -f1)
echo "$log_file: $SIZE"
done
# for - числовой диапазон
for i in {1..10}; do
echo "Итерация $i"
done
# while - пока условие истинно
COUNTER=0
while [ $COUNTER -lt 5 ]; do
echo "Счётчик: $COUNTER"
COUNTER=$((COUNTER + 1))
done
# while - читать файл построчно
while IFS= read -r line; do
echo "Строка: $line"
done < /etc/hosts
Чтение файла построчно через while read - стандартный идиом bash. IFS= сбрасывает разделитель полей чтобы строки с пробелами читались корректно. -r отключает интерпретацию обратных слешей.
Heredoc: многострочный ввод и генерация файлов
Heredoc - способ передать многострочный текст команде или записать в файл прямо в скрипте:
#!/bin/bash
# Создать конфиг-файл
cat > /etc/myapp/config.ini << 'EOF'
[database]
host = localhost
port = 5432
name = myapp
[server]
port = 8080
debug = false
EOF
echo "Конфиг создан"
# Передать SQL в psql
psql -U admin mydb << 'EOF'
CREATE TABLE IF NOT EXISTS events (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
EOF
echo "Таблица создана"
Одинарные кавычки вокруг EOF ('EOF') отключают интерпретацию переменных внутри heredoc - то что написано остаётся как есть. Без кавычек (EOF) - переменные подставляются. Используй одинарные кавычки когда пишешь конфиг с символами доллара, двойные кавычки или обратные слеши.
Запуск bash скрипта для автоматизации сервисов
Одно из частых применений bash - запуск нескольких сервисов одной командой. Особенно полезно при старте окружения разработки или при разворачивании стека на сервере.
Последовательный запуск нескольких программ
#!/bin/bash
set -euo pipefail
echo "=== Запуск стека ==="
echo "Время: $(date '+%Y-%m-%d %H:%M:%S')"
# Nginx
systemctl start nginx
echo "[OK] nginx"
# PostgreSQL
systemctl start postgresql
echo "[OK] postgresql"
# Redis
systemctl start redis
echo "[OK] redis"
echo "=== Стек запущен ==="
Запуск с проверкой результата
#!/bin/bash
set -euo pipefail
start_service() {
local service=$1
systemctl start "$service"
if systemctl is-active --quiet "$service"; then
echo "[OK] $service - запущен"
return 0
else
echo "[FAIL] $service - не запустился"
systemctl status "$service" --no-pager
return 1
fi
}
# Запускаем и проверяем
start_service nginx
start_service postgresql
start_service redis
echo "Все сервисы запущены."
Функция start_service запускает сервис и сразу проверяет статус через systemctl is-active. Если сервис не поднялся - показывает статус и возвращает код ошибки. При set -e скрипт остановится на первой ошибке - не будет запускать остальные сервисы на неработающем стеке.
Параллельный запуск с ожиданием
#!/bin/bash
# Запустить несколько процессов параллельно
python3 /home/user/app/worker1.py &
WORKER1_PID=$!
python3 /home/user/app/worker2.py &
WORKER2_PID=$!
echo "Воркеры запущены: PID $WORKER1_PID, PID $WORKER2_PID"
# Ждём завершения обоих
wait $WORKER1_PID
echo "Воркер 1 завершён"
wait $WORKER2_PID
echo "Воркер 2 завершён"
Символ & запускает процесс в фоне. $! - PID последнего запущенного фонового процесса. wait PID блокирует скрипт до завершения процесса с этим PID. Полезно когда нужно параллельно запустить несколько задач и дождаться когда все закончатся.
Запуск bash скрипта по расписанию через cron
Cron - встроенный планировщик задач Linux. Каждый пользователь имеет свой crontab - список задач с расписанием. Демон crond читает его и запускает задачи в нужное время.
Синтаксис crontab
# Формат: минуты часы день_месяца месяц день_недели команда
# * * * * * /path/script.sh
# Примеры:
# 0 * * * * - каждый час ровно
# */5 * * * * - каждые 5 минут
# 0 3 * * * - каждый день в 03:00
# 0 3 * * 1 - каждый понедельник в 03:00
# 0 3 1 * * - 1-го числа каждого месяца в 03:00
# 30 8 * * 1-5 - по будням в 8:30
# @reboot - один раз при загрузке системы
Открыть редактор crontab
crontab -e
Первый раз cron спросит какой редактор использовать. Если не знаком с vim - выбери nano (обычно цифра 1).
# Посмотреть текущие задачи
crontab -l
# Удалить все задачи (будет запрос подтверждения)
crontab -r
Практические примеры cron задач
# Бэкап БД каждую ночь в 3:00
0 3 * * * /home/user/scripts/backup_db.sh >> /var/log/backup_db.log 2>&1
# Очистка временных файлов каждое воскресенье в 4:00
0 4 * * 0 /home/user/scripts/clean_tmp.sh >> /var/log/clean.log 2>&1
# Проверка дискового пространства каждые 10 минут
*/10 * * * * /home/user/scripts/check_disk.sh >> /var/log/disk_check.log 2>&1
# Перезапуск сервиса каждое утро в 6:00
0 6 * * * /bin/systemctl restart myapp >> /var/log/myapp_restart.log 2>&1
# При загрузке системы
@reboot /home/user/scripts/startup.sh >> /var/log/startup.log 2>&1
Конструкция >> /var/log/script.log 2>&1 обязательна для любой cron-задачи которую хочешь контролировать. >> дописывает вывод в файл (не перезаписывает). 2>&1 перенаправляет stderr туда же куда идёт stdout. Без этого все ошибки уходят в никуда - или в почту root, которую никто не читает.
Почему скрипт не работает в cron хотя вручную работает
Классика жанра. Скрипт в терминале отработал отлично. В cron - ни звука. Причина в 99% случаев - окружение.
Cron запускает задачи с минимальным окружением. Переменная PATH в cron содержит только /usr/bin:/bin. Никакого /usr/local/bin, никакого пользовательского ~/.local/bin. Команда python3, node, pip, docker - могут быть не найдены.
Три правила для cron которые снимут 90% проблем
1. Полные пути к исполняемым файлам: не python3, а /usr/bin/python3
2. Полные пути к файлам скрипта: не ./backup.sh, а /home/user/scripts/backup.sh
3. Вывод в лог: >> /var/log/script.log 2>&1 — обязательно для отладки
Как найти полный путь команды:
which python3
# /usr/bin/python3
which node
# /usr/local/bin/node
Второй способ - добавить нужный PATH прямо в начало скрипта:
#!/bin/bash
# Явно задаём PATH для cron
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# Теперь python3, node, docker - все будут найдены
python3 /home/user/app/main.py
Третий способ - задать PATH прямо в crontab, в самом начале, до первой задачи:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1
Отладка cron задач
Скрипт в cron не работает, лог пустой. Значит скрипт вообще не запустился - или запустился, но что-то пошло не так ещё до твоих команд.
Первое - проверь что cron вообще запущен:
systemctl status cron
# или
systemctl status crond # на некоторых дистрибутивах
Второе - смотри системный лог cron:
grep CRON /var/log/syslog | tail -20
# или на системах с journald
journalctl -u cron --since "1 hour ago"
Там будет запись о каждом запуске задачи - даже если скрипт ничего не вывел. Если записи нет - задача не запустилась вообще (проверь синтаксис crontab).
Третье - протестируй скрипт с окружением cron. Запусти скрипт с минимальным PATH вручную:
env -i PATH=/usr/bin:/bin bash /home/user/scripts/script.sh
env -i запускает команду с пустым окружением. Если скрипт завалился здесь - проблема в PATH или переменных окружения. Если работает - проблема в crontab записи.
Чек-лист отладки cron задачи
Скрипт не работает в cron. Пройдись по списку по порядку.
| Что проверить |
Как проверить |
Что сделать |
| cron вообще запущен |
systemctl status cron |
systemctl start cron |
| Задача есть в crontab |
crontab -l |
crontab -e, добавь задачу |
| Синтаксис crontab правильный |
Проверь на crontab.guru |
Исправь расписание |
| Скрипт существует по указанному пути |
ls -la /полный/путь/script.sh |
Поправь путь в crontab |
| Скрипт исполняемый |
ls -la script.sh (должен быть x) |
chmod +x script.sh |
| Лог пишется |
cat /var/log/script.log |
Добавь >> /var/log/script.log 2>&1 |
| Системный лог cron |
grep CRON /var/log/syslog | tail -20 |
Смотри ошибки запуска |
| Скрипт работает с cron-окружением |
env -i PATH=/usr/bin:/bin bash script.sh |
Добавь PATH в скрипт или используй полные пути |
Альтернатива cron: systemd timers
Systemd timers - более современный способ планировки. Плюсы: зависимости между сервисами, логирование через journald, точная настройка тайминга. Минус: сложнее настраивать чем cron.
Создай два файла:
Сервис /etc/systemd/system/backup.service:
[Unit]
Description=Database Backup
[Service]
Type=oneshot
ExecStart=/home/user/scripts/backup_db.sh
User=backup
StandardOutput=journal
StandardError=journal
Таймер /etc/systemd/system/backup.timer:
[Unit]
Description=Daily Database Backup Timer
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable backup.timer
sudo systemctl start backup.timer
# Проверить все таймеры
systemctl list-timers
# Логи
journalctl -u backup.service
Persistent=true означает: если система была выключена в момент когда задача должна была запуститься - запустить при следующей загрузке. Cron так не умеет.
Запуск bash скрипта из Python
Python и bash - хорошая пара. Python берёт на себя логику, работу с API, обработку данных. Bash - системные операции, управление файлами, запуск утилит. Запустить bash из Python можно тремя способами.
subprocess.run - правильный способ
import subprocess
# Запустить скрипт
result = subprocess.run(["bash", "script.sh"])
# Запустить и получить вывод
result = subprocess.run(
["bash", "script.sh"],
capture_output=True,
text=True
)
print("Вывод:", result.stdout)
print("Ошибки:", result.stderr)
print("Код возврата:", result.returncode)
capture_output=True перехватывает stdout и stderr. text=True возвращает строки вместо байт - не надо декодировать вручную.
Запуск произвольной shell-команды строкой:
result = subprocess.run(
"ls -la /home/user | grep -v '^d'",
shell=True,
capture_output=True,
text=True
)
print(result.stdout)
Параметр shell=True передаёт строку в shell как есть - работают пайпы, перенаправления, glob-шаблоны. Без него нужно передавать список аргументов. Для команд с пайпами и перенаправлениями - используй shell=True. Для безопасного запуска с аргументами от пользователя - список без shell=True (иначе shell injection).
subprocess.Popen - для долгих процессов
subprocess.run ждёт завершения процесса. Если скрипт долгий и нужно читать его вывод в реальном времени - используй Popen:
import subprocess
process = subprocess.Popen(
["bash", "long_script.sh"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# Читаем вывод построчно по мере поступления
for line in process.stdout:
print(line, end="")
process.wait()
print(f"Завершён с кодом: {process.returncode}")
Это работает потому что process.stdout - это файлоподобный объект. Итерация по нему блокируется до появления новой строки. Как только скрипт пишет в stdout - Python немедленно это получает и выводит.
Полная обёртка для продакшна
import subprocess
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def run_bash_script(script_path, *args, timeout=60):
"""
Запустить bash скрипт и вернуть вывод.
Args:
script_path: полный путь к скрипту
*args: аргументы для скрипта
timeout: таймаут в секундах (по умолчанию 60)
Returns:
str: вывод скрипта (stdout)
Raises:
RuntimeError: если скрипт завершился с ненулевым кодом
subprocess.TimeoutExpired: если превышен таймаут
"""
cmd = ["bash", script_path] + list(args)
logger.info(f"Запуск: {' '.join(cmd)}")
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout
)
except subprocess.TimeoutExpired:
logger.error(f"Таймаут {timeout}с: {script_path}")
raise
if result.returncode != 0:
logger.error(f"Скрипт вернул код {result.returncode}")
logger.error(f"stderr: {result.stderr}")
raise RuntimeError(
f"Скрипт завершился с ошибкой (код {result.returncode}):\n"
f"{result.stderr}"
)
logger.info("Скрипт завершён успешно")
return result.stdout.strip()
# Использование
try:
output = run_bash_script(
"/home/user/scripts/backup.sh",
"--database", "mydb",
"--dest", "/backup"
)
print(f"Бэкап успешен:\n{output}")
except RuntimeError as e:
print(f"Ошибка: {e}")
except subprocess.TimeoutExpired:
print("Скрипт завис - превышен таймаут")
subprocess в Python: продвинутые сценарии
Стандартные случаи покрыты выше. Но есть несколько нюансов которые выплывают в реальных проектах.
Передача stdin скрипту из Python:
import subprocess
# Передать данные в stdin скрипта
result = subprocess.run(
["bash", "process_input.sh"],
input="строка1\nстрока2\nстрока3",
capture_output=True,
text=True
)
print(result.stdout)
Выполнение bash-команды с переменными окружения:
import subprocess
import os
# Добавить переменные к текущему окружению
env = os.environ.copy()
env["DB_HOST"] = "localhost"
env["DB_PORT"] = "5432"
env["APP_ENV"] = "production"
result = subprocess.run(
["bash", "deploy.sh"],
capture_output=True,
text=True,
env=env
)
Перехват вывода в реальном времени с таймаутом:
import subprocess
import threading
def stream_output(process):
for line in process.stdout:
print(f"[bash] {line.rstrip()}")
process = subprocess.Popen(
["bash", "long_job.sh"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # stderr в тот же поток что stdout
text=True,
bufsize=1 # построчная буферизация
)
# Читаем вывод в отдельном потоке
reader = threading.Thread(target=stream_output, args=(process,))
reader.start()
try:
process.wait(timeout=300) # ждём максимум 5 минут
except subprocess.TimeoutExpired:
process.kill()
print("Скрипт убит по таймауту")
finally:
reader.join()
print(f"Код выхода: {process.returncode}")
stderr=subprocess.STDOUT перенаправляет stderr в тот же поток что stdout. Удобно когда хочешь видеть весь вывод в порядке появления, а не раздельно.
Асинхронный запуск через asyncio - для современных Python-приложений:
import asyncio
async def run_script(script_path):
proc = await asyncio.create_subprocess_exec(
"bash", script_path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
raise RuntimeError(f"Ошибка: {stderr.decode()}")
return stdout.decode().strip()
# Запуск нескольких скриптов параллельно
async def main():
tasks = [
run_script("/home/user/scripts/backup_db.sh"),
run_script("/home/user/scripts/sync_files.sh"),
run_script("/home/user/scripts/check_certs.sh"),
]
results = await asyncio.gather(*tasks, return_exceptions=True)
for i, result in enumerate(results):
if isinstance(result, Exception):
print(f"Задача {i+1} упала: {result}")
else:
print(f"Задача {i+1}: {result[:50]}")
asyncio.run(main())
Asyncio полезно когда нужно параллельно запустить несколько долгих скриптов из Python-приложения без создания потоков вручную.
os.system - когда нужно просто запустить
import os
# Запустить - возвращает только код возврата
exit_code = os.system("bash script.sh")
if exit_code != 0:
print(f"Ошибка: код {exit_code}")
os.system простой, но не даёт перехватить вывод. Скрипт пишет прямо в терминал. Для новых проектов используй subprocess - гибче и даёт контроль над stdout/stderr. os.system оставь для быстрых однострочников.
Запуск bash скриптов на Raspberry Pi
Raspberry Pi работает под Raspberry Pi OS - это Debian. Всё описанное выше работает напрямую. Но у RPi есть своя специфика - управление GPIO через bash.
Управление GPIO через bash
Для работы с GPIO через bash нужна утилита gpio из пакета wiringpi:
# Установка
sudo apt install wiringpi
# Проверить установку
gpio -v
Внимание: WiringPi и новые модели RPi
WiringPi официально прекратил развитие, но неофициальные форки продолжают поддерживаться. На Raspberry Pi 4 и 5 используй форк с github.com/WiringPi/WiringPi. Для более современного подхода — библиотека pigpio или raspi-gpio.
Основные команды:
# Настроить пин 17 как выход
gpio -g mode 17 out
# Установить HIGH (3.3V)
gpio -g write 17 1
# Установить LOW (0V)
gpio -g write 17 0
# Прочитать состояние пина
gpio -g read 17
# Настроить пин как вход с подтягивающим резистором
gpio -g mode 18 in
gpio -g mode 18 up # pull-up
Флаг -g означает использование BCM-нумерации пинов (GPIO номера). Без него используется нумерация WiringPi - другая схема. BCM-нумерация указана на распиновке Raspberry Pi и используется во всей документации - используй -g.
Мигание светодиода
#!/bin/bash
PIN=17
# Инициализация пина
gpio -g mode $PIN out
# Корректное завершение по Ctrl+C
cleanup() {
gpio -g write $PIN 0
echo ""
echo "Остановлено. Светодиод выключен."
exit 0
}
trap cleanup SIGINT SIGTERM
echo "Мигание на пине $PIN. Ctrl+C для остановки."
while true; do
gpio -g write $PIN 1
sleep 0.5
gpio -g write $PIN 0
sleep 0.5
done
Конструкция trap cleanup SIGINT SIGTERM - обработчик сигналов. Когда нажимаешь Ctrl+C, bash получает SIGINT. Без trap скрипт завершится немедленно, светодиод останется в последнем состоянии. С trap перед завершением выполняется функция cleanup - выключает пин. Всегда добавляй trap в GPIO-скрипты.
Скрипт мониторинга температуры
#!/bin/bash
THRESHOLD=70
LOG="/var/log/rpi_temp.log"
get_temp() {
vcgencmd measure_temp | grep -oP '\d+\.\d+'
}
log_temp() {
local temp=$1
echo "$(date '+%Y-%m-%d %H:%M:%S') - CPU: ${temp}C" >> "$LOG"
}
TEMP=$(get_temp)
log_temp "$TEMP"
echo "Температура CPU: ${TEMP}°C"
# Сравниваем через bc (bc умеет float)
if (( $(echo "$TEMP > $THRESHOLD" | bc -l) )); then
echo "ПРЕДУПРЕЖДЕНИЕ: перегрев ${TEMP}°C > ${THRESHOLD}°C"
# Здесь можно: отправить уведомление, включить вентилятор GPIO
fi
Запускай через cron каждые 5 минут:
# crontab -e
*/5 * * * * /home/pi/scripts/check_temp.sh >> /var/log/temp_monitor.log 2>&1
Автозапуск скрипта при загрузке RPi
Три способа - от простого к правильному.
Способ 1 - cron @reboot. Просто, работает:
crontab -e
# Добавить:
@reboot /home/pi/scripts/gpio_init.sh >> /var/log/gpio_init.log 2>&1
Минус: нет зависимостей, нет проверки завершения, нет удобных логов.
Способ 2 - systemd service. Правильно для серьёзных задач:
# /etc/systemd/system/gpio-init.service
[Unit]
Description=GPIO Initialization
After=multi-user.target
[Service]
Type=oneshot
ExecStart=/home/pi/scripts/gpio_init.sh
User=pi
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable gpio-init.service
sudo systemctl start gpio-init.service
# Статус
sudo systemctl status gpio-init.service
# Логи
journalctl -u gpio-init.service
Способ 3 - /etc/rc.local. Старый, но работает на всех дистрибутивах:
sudo nano /etc/rc.local
# Добавить перед строкой "exit 0":
/home/pi/scripts/gpio_init.sh &
Практический пример: скрипт резервного копирования
Теория - хорошо. Но лучше посмотреть как всё это собирается в рабочий скрипт. Скрипт бэкапа базы данных - классика которая пригодится любому.
#!/bin/bash
set -euo pipefail
# ============================================================
# backup_db.sh - бэкап PostgreSQL базы данных
# Использование: ./backup_db.sh [директория_бэкапа]
# ============================================================
# Конфигурация
DB_NAME="${1:-myapp}"
BACKUP_DIR="${2:-/backup/postgres}"
KEEP_DAYS=7
LOG_FILE="/var/log/backup_db.log"
DATE=$(date +%Y-%m-%d_%H-%M-%S)
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${DATE}.sql.gz"
# Логирование
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [$1] $2" | tee -a "$LOG_FILE"
}
# Проверка зависимостей
check_deps() {
for cmd in pg_dump gzip; do
if ! command -v "$cmd" > /dev/null 2>&1; then
log "ERROR" "Не найдена команда: $cmd"
exit 1
fi
done
}
# Создание директории если не существует
prepare_dir() {
if [ ! -d "$BACKUP_DIR" ]; then
mkdir -p "$BACKUP_DIR"
log "INFO" "Создана директория: $BACKUP_DIR"
fi
}
# Бэкап
do_backup() {
log "INFO" "Начинаем бэкап базы $DB_NAME"
pg_dump "$DB_NAME" | gzip > "$BACKUP_FILE"
local size
size=$(du -h "$BACKUP_FILE" | cut -f1)
log "INFO" "Бэкап создан: $BACKUP_FILE ($size)"
}
# Удаление старых бэкапов
cleanup_old() {
log "INFO" "Удаляем бэкапы старше $KEEP_DAYS дней"
find "$BACKUP_DIR" -name "${DB_NAME}_*.sql.gz" -mtime +"$KEEP_DAYS" -delete
log "INFO" "Очистка завершена"
}
# Основная логика
check_deps
prepare_dir
do_backup
cleanup_old
log "INFO" "Готово"
Что здесь важно.
set -euo pipefail в начале - скрипт остановится если pg_dump или gzip завершится с ошибкой. Без этого сломанный архив тихо запишется в файл.
command -v "$cmd" > /dev/null - правильный способ проверить доступность команды. Лучше чем which: работает для встроенных команд bash, не зависит от наличия which в системе.
"${1:-myapp}" - значение по умолчанию. Если первый аргумент не передан - используется myapp. Избавляет от ручной проверки if [ -z "$1" ].
Функции - удобно и читаемо. Логика разбита на части. Каждую функцию можно тестировать отдельно.
Скрипт мониторинга дискового пространства
#!/bin/bash
set -euo pipefail
THRESHOLD=85
HOSTNAME=$(hostname)
LOG="/var/log/disk_monitor.log"
check_disk() {
local mount_point="$1"
local usage
# df возвращает % без знака %
usage=$(df "$mount_point" | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
echo "$(date '+%Y-%m-%d %H:%M:%S') $mount_point: ${usage}%" >> "$LOG"
if [ "$usage" -gt "$THRESHOLD" ]; then
echo "ALERT: $HOSTNAME - $mount_point заполнен на ${usage}% (порог: ${THRESHOLD}%)"
# Сюда: отправка email, telegram, slack уведомления
return 1
fi
return 0
}
# Проверяем несколько точек монтирования
EXIT_CODE=0
for mount in / /var /home /tmp; do
if mountpoint -q "$mount" 2>/dev/null; then
check_disk "$mount" || EXIT_CODE=1
fi
done
exit $EXIT_CODE
Запускай через cron каждые 10 минут:
*/10 * * * * /home/user/scripts/check_disk.sh >> /var/log/disk_monitor.log 2>&1
Безопасность bash скриптов
Скрипты в продакшне - это код. К нему применяются те же требования что к любому коду: не доверяй вводу, минимум привилегий, защита секретов.
Не храни секреты в скрипте
#!/bin/bash
# ПЛОХО - пароль в скрипте
pg_dump -U admin -W "mypassword123" mydb > backup.sql
# ХОРОШО - пароль из переменной окружения
pg_dump -U "${DB_USER}" mydb > backup.sql
# DB_PASSWORD передаётся через environment, не в скрипте
# ХОРОШО - пароль из файла с правильными правами
DB_PASSWORD=$(cat /etc/myapp/.db_password)
# Файл с правами 600: chmod 600 /etc/myapp/.db_password
# ХОРОШО - pgpass для PostgreSQL
# ~/.pgpass: hostname:port:database:username:password
# chmod 600 ~/.pgpass
pg_dump -U admin mydb > backup.sql
Квотирование переменных с пользовательским вводом
#!/bin/bash
# Получаем имя файла от пользователя
FILENAME="$1"
# ПЛОХО - shell injection
rm $FILENAME
# Если FILENAME="-rf / " - это катастрофа
# ХОРОШО - кавычки защищают от пробелов и спецсимволов
rm "$FILENAME"
# ЛУЧШЕ - проверять ввод перед использованием
if [[ "$FILENAME" =~ ^[a-zA-Z0-9._-]+$ ]]; then
rm "$FILENAME"
else
echo "Недопустимые символы в имени файла"
exit 1
fi
Минимум привилегий
# Не запускай скрипты от root без необходимости
# Если нужны права root для конкретной команды - используй sudo для этой команды
#!/bin/bash
# ПЛОХО - запускать весь скрипт от root
# sudo ./script.sh
# ХОРОШО - повышение привилегий только там где нужно
echo "Обрабатываем файлы..."
process_files # без root
echo "Перезапускаем сервис..."
sudo systemctl restart nginx # только здесь root
echo "Готово"
Временные файлы
#!/bin/bash
# ПЛОХО - предсказуемое имя временного файла
TMP_FILE=/tmp/script_temp.txt
# ХОРОШО - уникальное временное имя
TMP_FILE=$(mktemp /tmp/script.XXXXXX)
# Гарантированное удаление при выходе
cleanup() {
rm -f "$TMP_FILE"
}
trap cleanup EXIT
# Работаем с временным файлом
some_command > "$TMP_FILE"
process "$TMP_FILE"
trap cleanup EXIT выполняет функцию cleanup при любом завершении скрипта - нормальном, по ошибке (set -e), по сигналу. Временный файл гарантированно удалится.
Чтение состояния GPIO: кнопки и датчики
GPIO работает в обе стороны - не только вывод, но и ввод. Кнопки, датчики, концевые выключатели.
#!/bin/bash
BUTTON_PIN=18
LED_PIN=17
# Настраиваем пины
gpio -g mode $BUTTON_PIN in
gpio -g mode $BUTTON_PIN up # подтягивающий резистор к VCC
gpio -g mode $LED_PIN out
echo "Нажми кнопку на пине $BUTTON_PIN..."
while true; do
# Читаем состояние кнопки
STATE=$(gpio -g read $BUTTON_PIN)
# При подтягивающем резисторе: 0 = нажата, 1 = отпущена
if [ "$STATE" -eq 0 ]; then
gpio -g write $LED_PIN 1
echo "Кнопка нажата - светодиод включён"
else
gpio -g write $LED_PIN 0
fi
sleep 0.05 # 50мс опрос
done
Скрипт управления GPIO с аргументами
Универсальный скрипт для управления пинами - удобно вызывать из Python или другого скрипта:
#!/bin/bash
# gpio_ctrl.sh - управление GPIO пином
# Использование: ./gpio_ctrl.sh [value]
# Действия: init_out, init_in, write, read, toggle
set -euo pipefail
PIN="${1:-17}"
ACTION="${2:-read}"
VALUE="${3:-0}"
case "$ACTION" in
init_out)
gpio -g mode "$PIN" out
echo "Пин $PIN: настроен как выход"
;;
init_in)
gpio -g mode "$PIN" in
gpio -g mode "$PIN" up
echo "Пин $PIN: настроен как вход с подтяжкой"
;;
write)
gpio -g write "$PIN" "$VALUE"
echo "Пин $PIN: записано $VALUE"
;;
read)
STATE=$(gpio -g read "$PIN")
echo "$STATE"
;;
toggle)
CURRENT=$(gpio -g read "$PIN")
NEW=$(( 1 - CURRENT ))
gpio -g write "$PIN" "$NEW"
echo "Пин $PIN: $CURRENT -> $NEW"
;;
*)
echo "Неизвестное действие: $ACTION"
echo "Действия: init_out, init_in, write, read, toggle"
exit 1
;;
esac
Вызов из другого скрипта или Python:
# Включить реле на пине 17
./gpio_ctrl.sh 17 write 1
# Прочитать состояние кнопки
STATE=$(./gpio_ctrl.sh 18 read)
echo "Состояние кнопки: $STATE"
import subprocess
def gpio_write(pin, value):
subprocess.run(
["bash", "/home/pi/scripts/gpio_ctrl.sh", str(pin), "write", str(value)],
check=True
)
def gpio_read(pin):
result = subprocess.run(
["bash", "/home/pi/scripts/gpio_ctrl.sh", str(pin), "read"],
capture_output=True,
text=True,
check=True
)
return int(result.stdout.strip())
# Включить LED на пине 17
gpio_write(17, 1)
# Прочитать кнопку на пине 18
button_state = gpio_read(18)
print(f"Кнопка: {button_state}")
Запуск bash скриптов в Windows
Windows - не Linux, но запускать bash-скрипты там реально. Четыре способа в зависимости от задачи.
WSL - полноценный Linux в Windows
Windows Subsystem for Linux - лучший вариант для серьёзной работы. Полноценное ядро Linux, apt, все системные утилиты. Устанавливается из PowerShell от имени администратора:
wsl --install
По умолчанию установится Ubuntu. После перезагрузки создай пользователя и готово.
Запуск bash скрипта через WSL:
# Запустить bash напрямую
wsl bash /home/user/script.sh
# Запустить скрипт Windows-пути
wsl bash "//c/Users/User/scripts/script.sh"
# Из PowerShell, получить вывод в переменную
$output = wsl bash /home/user/script.sh
Write-Host $output
Скрипты с Linux-путями работают нативно. Для Windows-путей нужно конвертировать: C:\Scripts\ становится //c/Scripts/ или /mnt/c/Scripts/ внутри WSL.
Git Bash - для разработчиков
Устанавливается вместе с Git for Windows. Не нужно включать WSL, работает из коробки. Минус: нет полноценного apt, системные команды Linux отсутствуют.
Скачай Git for Windows: https://git-scm.com/download/win
# В терминале Git Bash
./script.sh
bash script.sh
# Запустить из CMD или PowerShell
"C:\Program Files\Git\bin\bash.exe" C:\Scripts\script.sh
Хороший выбор для переносимых скриптов которые не используют Linux-специфичные команды (systemctl, /proc, и т.д.).
Нюансы запуска bash в WSL: пути и права
WSL запускает настоящее Linux-ядро, большинство скриптов работают без изменений. Но есть нюансы.
Пути к файлам. Диск C: в WSL доступен как /mnt/c/. Скрипт который работает с Windows-файлами должен использовать этот путь:
# Файл C:\Users\User\Documents\data.csv доступен в WSL как:
cat /mnt/c/Users/User/Documents/data.csv
Права на файлы. NTFS не поддерживает Unix-права в полной мере. Файлы в /mnt/c/ часто имеют права 777. chmod на них работает непредсказуемо. Для скриптов которые должны быть исполняемыми - держи их в файловой системе WSL (/home/user/), не в Windows-разделе.
Производительность. Файловые операции в /mnt/c/ значительно медленнее чем в Linux-части WSL (/home/user/). Если скрипт обрабатывает много файлов - работай с копией в /home/.
Cygwin - альтернатива WSL
POSIX-совместимая среда для Windows. Старше WSL, иногда нужен для специфических задач совместимости. Установи с cygwin.com, запускай скрипты как в Linux.
Ограничение: Cygwin не использует настоящее ядро Linux - это эмуляция POSIX API. Скрипты работают, но производительность ниже WSL и бинарники Linux там не запустишь.
Конвертация CRLF в LF
Создал скрипт в Windows - получил ошибку при запуске в Linux:
/bin/bash^M: bad interpreter
^M - это символ возврата каретки CR (ASCII 13). Windows использует CRLF (\r\n) как конец строки, Linux - LF (\n). Shebang с \r на конце превращается в неправильный путь.
Решение:
# Утилита dos2unix
sudo apt install dos2unix
dos2unix script.sh
# Или через sed
sed -i 's/\r//' script.sh
# Или через vim
vim script.sh
# :set ff=unix
# :wq
Настрой редактор чтобы сразу сохранять в Unix-формате. В VS Code - правый нижний угол, там показан текущий формат строк (CRLF или LF). Кликни и измени.
Типичные ошибки при запуске bash скрипта
Permission denied
bash: ./script.sh: Permission denied
Причина: на файле нет флага исполняемости.
Решение:
chmod +x script.sh
./script.sh
# Или без chmod
bash script.sh
Дополнительный случай: скрипт на примонтированном диске с опцией noexec. Тогда даже chmod +x не поможет. Проверь:
mount | grep "noexec"
Если скрипт на примонтированном разделе с noexec - запускай через явный вызов bash:
bash /mnt/backup/script.sh
Command not found
./script.sh: line 5: python3: command not found
Причина 1: программа не установлена. Проверь:
which python3
# если пустой вывод - не установлена
Причина 2: команда не в PATH (часто при запуске из cron). Проверь где находится программа и используй полный путь:
which python3
# /usr/bin/python3
# В скрипте пиши полный путь:
/usr/bin/python3 /home/user/app/main.py
Скрипт не находит файлы: проблема рабочей директории
cat: config.txt: No such file or directory
Причина: скрипт запущен не из своей директории. config.txt ищется в рабочей директории откуда запустили скрипт, а не там где лежит скрипт.
Решение - в начале скрипта перейти в его директорию:
#!/bin/bash
# Определить директорию скрипта и перейти в неё
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Теперь относительные пути работают от директории скрипта
cat config.txt # найдёт файл рядом со скриптом
python3 helper.py # запустит скрипт из той же директории
${BASH_SOURCE[0]} - путь к текущему скрипту. Работает при прямом запуске, через source, через bash. Надёжнее чем $0.
Скрипт не останавливается при ошибке
По умолчанию bash продолжает выполнение даже если команда вернула ошибку. Это поведение по умолчанию приводит к катастрофическим последствиям в деструктивных операциях - скрипт "удалил" директорию, получил ошибку, пошёл дальше и сделал что-то непоправимое.
#!/bin/bash
# Добавь в начало каждого продакшн-скрипта
set -e # завершить скрипт при любой ошибке
set -u # завершить при использовании неопределённой переменной
set -o pipefail # код ошибки pipeline = код последней упавшей команды
# Кратко:
set -euo pipefail
Что каждый флаг делает:
set -e: если команда вернула ненулевой код - скрипт завершается. Исключения: команды в if, после ||, в условии while.
set -u: использование необъявленной переменной - ошибка. Защищает от опечаток в именах переменных. Без этого $USRE вместо $USER тихо подставит пустую строку.
set -o pipefail: в конвейере cmd1 | cmd2 код возврата - код последней команды. Без pipefail если cmd1 упала, а cmd2 отработала нормально - конвейер считается успешным. С pipefail - нет.
Переменные с пробелами
# Проблема
DIR=/home/user/my folder # директория с пробелом
cd $DIR # bash воспримет это как: cd /home/user/my folder (два аргумента)
# Решение: всегда кавычки вокруг переменных
cd "$DIR"
# То же с массивами и глобами
FILES=*.log
rm $FILES # может не сработать как ожидается
rm "$FILES" # кавычки сохранят шаблон
Правило: переменные в bash всегда в двойных кавычках если не уверен что там нет пробелов. "$VAR" вместо $VAR. Исключение - числовые переменные в арифметических выражениях.
Heredoc и CRLF в cron
Heredoc - удобная конструкция для многострочного ввода. Частая ловушка: если файл скрипта с heredoc создан на Windows, CRLF в heredoc приводит к неожиданным результатам:
# Правильный heredoc
cat > /tmp/config.txt << 'EOF'
server = localhost
port = 5432
EOF
# Если скрипт в CRLF формате - heredoc будет содержать \r в конце строк
# Решение: dos2unix на файл скрипта
dos2unix script.sh
Bash в связке с другими инструментами
Bash редко живёт сам по себе. В реальных задачах он работает в связке.
Bash + Python: правильное разделение
Практическое правило: bash для последовательности операций и вызова системных утилит, Python для логики, работы с данными, API-запросов. Когда Python вызывает bash - он получает лучшее от обоих.
Типичный шаблон: Python собирает параметры и данные, bash выполняет системные операции:
import subprocess
import json
from datetime import datetime
def deploy_service(service_name, version, config):
# Python: подготовка параметров, валидация
if not service_name.isalnum():
raise ValueError(f"Недопустимое имя сервиса: {service_name}")
deploy_params = json.dumps({
"service": service_name,
"version": version,
"timestamp": datetime.now().isoformat()
})
# Bash: системные операции
result = subprocess.run(
["bash", "/opt/scripts/deploy.sh", service_name, version],
capture_output=True,
text=True,
env={**__import__('os').environ, "DEPLOY_CONFIG": deploy_params}
)
if result.returncode != 0:
raise RuntimeError(f"Деплой упал:\n{result.stderr}")
return result.stdout
Bash + systemd: правильный автозапуск
Cron @reboot прост, но systemd даёт больше контроля. Для продакшн-скриптов которые должны работать постоянно или запускаться при старте - systemd.
Отличие от cron @reboot: systemd отслеживает процесс. Если скрипт упал - можно настроить автоматический рестарт. Cron @reboot запустил и забыл.
# /etc/systemd/system/monitor.service
[Unit]
Description=Server Monitor Script
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=monitor
ExecStart=/home/monitor/scripts/monitor.sh
Restart=on-failure
RestartSec=30s
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Restart=on-failure - перезапустить если скрипт завершился с ненулевым кодом. RestartSec=30s - пауза перед рестартом. After=network-online.target - запускать только когда сеть готова. Всего этого нет в cron.
Bash + Ansible: автоматизация инфраструктуры
Ansible управляет конфигурацией через YAML-описания и SSH. Внутри Ansible-задач можно вызывать bash-скрипты через модуль script или shell. Это полезно для операций которые сложно описать через Ansible-модули но легко - через bash.
# В Ansible playbook:
- name: Запустить кастомный скрипт конфигурации
script: /local/path/setup.sh arg1 arg2
args:
executable: /bin/bash
Это не замена нативным Ansible-модулям - они идемпотентны, a скрипты - нет. Но для миграций, первоначальной настройки и операций без готового модуля - работает.
Диагностика за 2 минуты: скрипт не работает
Скрипт упал. Пять шагов от симптома к причине.
Шаг 1: Запусти вручную и посмотри вывод. Звучит очевидно - но большинство "таинственных" ошибок обнаруживается при первом же ручном запуске в терминале.
bash script.sh
echo "Код выхода: $?"
Шаг 2: Включи трассировку.
bash -x script.sh 2>&1 | head -50
Смотришь на вывод и видишь: на какой команде остановился, с какими подставленными значениями переменных.
Шаг 3: Проверь окружение как у cron.
env -i PATH=/usr/bin:/bin HOME=/root bash script.sh
Если скрипт в этом окружении не работает - проблема в PATH или переменных окружения.
Шаг 4: Проверь синтаксис и распространённые ошибки.
bash -n script.sh
shellcheck script.sh
Шаг 5: Смотри системные логи.
# Cron логи
grep CRON /var/log/syslog | grep "script.sh" | tail -10
# Systemd логи
journalctl -u myscript.service --since "1 hour ago" --no-pager
В большинстве случаев проблема находится на шагах 1-3. Шаги 4-5 нужны для более хитрых ситуаций.
Полезные паттерны bash: что пригодится в реальных скриптах
Несколько конструкций которые встречаются постоянно. Выучи однажды - используй везде.
Проверка что скрипт запущен от root
#!/bin/bash
if [ "$EUID" -ne 0 ]; then
echo "Этот скрипт нужно запускать от root"
echo "Используй: sudo $0"
exit 1
fi
$EUID - эффективный User ID текущего процесса. 0 = root. Проверяй это в начале скрипта который требует привилегий - иначе скрипт молча упадёт на первой привилегированной команде.
Spinner и прогресс для долгих операций
#!/bin/bash
spinner() {
local pid=$1
local delay=0.1
local spinstr='|/-\'
while kill -0 "$pid" 2>/dev/null; do
local temp=${spinstr#?}
printf " [%c] " "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b\b\b\b"
done
printf " \b\b\b\b"
}
echo -n "Копирование файлов..."
cp -r /source /destination &
spinner $!
echo " готово"
Параллельное выполнение с ограничением числа процессов
#!/bin/bash
MAX_JOBS=4
PIDS=()
process_file() {
local file="$1"
# Обработка файла
sleep 1
echo "Обработан: $file"
}
for file in /data/*.csv; do
process_file "$file" &
PIDS+=($!)
# Ждём если запущено слишком много процессов
while [ ${#PIDS[@]} -ge $MAX_JOBS ]; do
for i in "${!PIDS[@]}"; do
if ! kill -0 "${PIDS[$i]}" 2>/dev/null; then
unset 'PIDS[$i]'
fi
done
PIDS=("${PIDS[@]}") # пересоздаём без пустых элементов
sleep 0.1
done
done
# Ждём завершения всех
wait
echo "Все файлы обработаны"
Mutex: защита от параллельного запуска
Когда скрипт запускается по cron и ты хочешь убедиться что не запущены два экземпляра одновременно:
#!/bin/bash
LOCK_FILE="/var/run/myscript.lock"
# Проверяем и создаём lock-файл атомарно
if ! mkdir "$LOCK_FILE" 2>/dev/null; then
PID=$(cat "$LOCK_FILE/pid" 2>/dev/null)
echo "Скрипт уже запущен (PID: $PID)"
exit 1
fi
# Сохраняем PID
echo $$ > "$LOCK_FILE/pid"
# Удаляем lock при выходе
cleanup() {
rm -rf "$LOCK_FILE"
}
trap cleanup EXIT
echo "Скрипт запущен, PID: $$"
# Основная логика скрипта
sleep 10
echo "Работа завершена"
mkdir - атомарная операция: либо создал директорию, либо нет. Используется как mutex. Надёжнее чем проверка через [ -f lock ] + создание файла - между двумя операциями может вклиниться второй процесс.
Retry: повторить при ошибке
#!/bin/bash
retry() {
local max_attempts="$1"
local delay="$2"
shift 2
local cmd="$@"
local attempt=1
while [ $attempt -le $max_attempts ]; do
echo "Попытка $attempt/$max_attempts: $cmd"
if eval "$cmd"; then
return 0
fi
if [ $attempt -lt $max_attempts ]; then
echo "Неудача. Повтор через ${delay}с..."
sleep "$delay"
fi
attempt=$((attempt + 1))
done
echo "Все $max_attempts попыток исчерпаны"
return 1
}
# Использование: до 3 попыток с паузой 5 секунд
retry 3 5 curl -f https://api.example.com/health
retry 3 10 pg_dump mydb > /backup/mydb.sql
Полезно для сетевых операций, для команд которые могут упасть из-за временной недоступности ресурса. Не надо усложнять скрипт вручной логикой - оберни команду в retry.
Отладка bash скриптов
Есть несколько инструментов для диагностики.
bash -x: трассировка выполнения
bash -x script.sh
Bash выводит каждую команду перед выполнением, со знаком +. Переменные подставлены. Видишь что именно выполняется и с какими значениями.
Включить трассировку только для части скрипта:
#!/bin/bash
echo "Начало"
set -x # включить трассировку
RESULT=$(some_complex_command)
echo "Результат: $RESULT"
set +x # выключить трассировку
echo "Конец"
bash -n: проверка синтаксиса
bash -n script.sh
Проверяет синтаксис без выполнения. Находит незакрытые кавычки, неправильные условия, пропущенные fi и done.
shellcheck: статический анализатор
# Установка
sudo apt install shellcheck
# Проверить скрипт
shellcheck script.sh
shellcheck - незаменимый инструмент. Находит не только синтаксические ошибки, но и логические: незакавыченные переменные, неправильное сравнение чисел, устаревшие конструкции. Если пишешь скрипты регулярно - поставь shellcheck.
Логирование в скрипте
#!/bin/bash
LOG="/var/log/myscript.log"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [$1] $2" | tee -a "$LOG"
}
log "INFO" "Скрипт запущен"
log "INFO" "Обрабатываем файлы..."
# Что-то делаем
if some_command; then
log "INFO" "Команда выполнена успешно"
else
log "ERROR" "Команда завершилась с ошибкой"
exit 1
fi
log "INFO" "Скрипт завершён"
tee -a "$LOG" одновременно пишет в файл и выводит в терминал. Удобно для скриптов которые запускаются и вручную, и через cron.
Шпаргалка: запуск bash скрипта
| Задача |
Команда |
| Дать права на выполнение |
chmod +x script.sh |
| Запустить из текущей папки |
./script.sh |
| Запустить без chmod |
bash script.sh |
| Запустить по полному пути |
/home/user/scripts/script.sh |
| Проверить синтаксис |
bash -n script.sh |
| Трассировка выполнения |
bash -x script.sh |
| Редактировать cron |
crontab -e |
| Посмотреть cron задачи |
crontab -l |
| Запустить из Python |
subprocess.run(["bash", "script.sh"]) |
| Запустить в Windows WSL |
wsl bash script.sh |
| Конвертировать CRLF в LF |
dos2unix script.sh |
| Статический анализ |
shellcheck script.sh |
FAQ
Почему bash скрипт не запускается через cron хотя вручную работает?
Почти всегда - проблема с PATH или с путями к файлам. Cron запускает задачи с минимальным PATH: только /usr/bin:/bin. Команды которые у тебя есть в интерактивной сессии - node, python3, docker - могут быть не найдены.
Решение: используй полные пути к исполняемым файлам (/usr/bin/python3 вместо python3) и полные пути к скриптам (/home/user/scripts/backup.sh вместо ./backup.sh). Обязательно добавь вывод в лог: >> /var/log/script.log 2>&1 - иначе ошибки уходят в никуда.
Как запустить bash скрипт с аргументами?
Аргументы передаются после имени скрипта через пробел, внутри скрипта доступны как $1, $2, и т.д.:
# Запуск с аргументами
./backup.sh mydb /backup/path
# Внутри скрипта
#!/bin/bash
DB_NAME=$1 # mydb
DEST_PATH=$2 # /backup/path
echo "Бэкап $DB_NAME в $DEST_PATH"
$0 - имя самого скрипта. $# - количество аргументов. $@ - все аргументы как массив.
Как запустить bash скрипт при загрузке Linux?
Три способа в порядке предпочтения: systemd service (правильно, гибко, логи через journald), cron @reboot (просто, работает везде), /etc/rc.local (совместимость со старыми системами). Для RPi или домашнего сервера проще всего cron @reboot - добавь @reboot /полный/путь/script.sh в crontab.
Как проверить что bash скрипт завершился с ошибкой?
После выполнения любой команды или скрипта код возврата хранится в $?:
bash script.sh
echo "Код возврата: $?"
# 0 = успех, не-0 = ошибка
Из Python: result.returncode у subprocess. Нулевой - успех, любой другой - ошибка. Добавь set -e в скрипт чтобы он сам останавливался при первой ошибке.
Зачем нужен shebang и что будет без него?
Shebang (#!/bin/bash) говорит операционной системе каким интерпретатором запускать файл. Без shebang при запуске ./script.sh система не знает что делать с файлом. Поведение зависит от системы: может запустить через /bin/sh (не bash, другой синтаксис), может выдать ошибку. При явном вызове bash script.sh shebang не нужен - ты сам указываешь интерпретатор. Но shebang лучше всегда писать: это документирует намерение и защищает от неожиданностей.
Чем subprocess.run отличается от os.system в Python?
os.system запускает команду через системный shell и возвращает только код возврата. Вывод идёт прямо в терминал - перехватить нельзя. subprocess.run гибче: можно перехватить stdout и stderr, передавать аргументы без shell injection, задавать таймаут, получать структурированный результат. Для новых проектов - всегда subprocess. os.system оставь для однострочников в интерактивном режиме.
Как передать переменные окружения в bash скрипт?
Три способа. Прямо перед командой - для одного вызова:
DB_HOST=localhost DB_PORT=5432 ./script.sh
Через export - для текущей сессии и дочерних процессов:
export DB_HOST=localhost
export DB_PORT=5432
./script.sh
Через .env файл с source:
# .env файл
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
# В скрипте или в терминале
source /path/to/.env
В cron переменные задаются прямо в crontab до первой задачи:
DB_HOST=localhost
DB_PORT=5432
0 3 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1
Как сделать bash скрипт интерактивным?
#!/bin/bash
# Запросить строку
read -p "Введите имя базы данных: " DB_NAME
echo "База: $DB_NAME"
# Запросить пароль (без эха)
read -sp "Введите пароль: " PASSWORD
echo ""
# С таймаутом - 5 секунд
read -t 5 -p "Продолжить? (y/N) " CONFIRM || true
if [[ "$CONFIRM" =~ ^[Yy]$ ]]; then
echo "Продолжаем"
else
echo "Отменено"
exit 0
fi
Как отловить ошибку в bash скрипте и выполнить действие?
Через trap на ERR - выполнит функцию при любой ошибке:
#!/bin/bash
set -euo pipefail
error_handler() {
local exit_code=$?
local line_number=$1
echo "Ошибка на строке $line_number (код: $exit_code)"
}
trap 'error_handler $LINENO' ERR
echo "Шаг 1"
false # ошибка - вызовется error_handler
echo "Этот вывод не появится"
Если команда упадёт - вызовется error_handler с номером строки, и только потом скрипт завершится.
Архитектура: как скрипт вписывается в систему
Bash скрипт - это не изолированная программа. Он всегда часть какой-то системы: запускается планировщиком, вызывается другой программой, управляет сервисами. Схема показывает типичные сценарии.
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
A["Cron / systemd timer"] --> B["bash script.sh"]
C["Python subprocess.run"] --> B
D["Ручной запуск терминал"] --> B
E["@reboot автозапуск"] --> B
B --> F["Системные утилиты nginx mysql pg_dump"]
B --> G["Файловая система логи бэкапы"]
B --> H["Сервисы systemctl start stop"]
B --> I["GPIO Raspberry Pi"]
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style C fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style D fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style E fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style B fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style F fill:#f8fafc,stroke:#94a3b8,stroke-width:2px,color:#1e293b
style G fill:#f8fafc,stroke:#94a3b8,stroke-width:2px,color:#1e293b
style H fill:#f8fafc,stroke:#94a3b8,stroke-width:2px,color:#1e293b
style I fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#c2410c
В центре - bash скрипт. Его вызывают сверху: cron по расписанию, Python когда нужны системные операции, ты сам из терминала. Снизу скрипт управляет: системными утилитами, файлами, сервисами. На Raspberry Pi - ещё и GPIO.
Задача скрипта - склеить эти части в нужной последовательности. Bash не заменяет ни cron, ни Python, ни systemd. Он между ними - как слой клея.
Что лучше: bash скрипт или Python для автоматизации?
Зависит от задачи. Bash хорош когда нужно: запустить несколько команд в последовательности, поработать с файлами через find/grep/awk, вызвать системные утилиты, написать быстро и просто. Python хорош когда нужно: обработать данные, работать с API, сложная логика с условиями и циклами, обработка ошибок с разными ветками.
На практике они дополняют друг друга. Python вызывает bash когда нужны системные операции. Bash вызывает Python когда нужна логика. Не надо выбирать - надо знать оба.
Как посмотреть что происходит в скрипте который уже запущен?
Если скрипт пишет в лог - смотри лог в реальном времени:
tail -f /var/log/myscript.log
Найти PID запущенного скрипта:
pgrep -f "script.sh"
ps aux | grep "script.sh"
Посмотреть что делает процесс прямо сейчас:
strace -p PID 2>&1 | head -30
strace показывает системные вызовы процесса - куда он обращается, что читает и пишет. Для диагностики зависших скриптов - незаменимо.
Итог
Bash скрипт запускается тремя командами: chmod +x, ./script.sh, и готово. Всё остальное - детали конкретных сценариев.
Запомни три вещи которые снимут большинство проблем. В cron - полные пути и вывод в лог. В скриптах - set -euo pipefail в начале чтобы скрипт не продолжал на сломанном стеке. В Python - subprocess.run с capture_output=True чтобы видеть что происходит.
Инструменты которые стоит поставить сразу: shellcheck для статического анализа скриптов (находит проблемы до запуска), dos2unix для конвертации файлов от Windows. Оба есть в apt.
Скрипты пишешь руками. Первые три будут с ошибками. Четвёртый уже будет работать с первого запуска. На двадцатом ловишь себя на том что добавляешь set -euo pipefail автоматически, не задумываясь. Это и есть мышечная память сетевого инженера.
Что-то не запускается?
Напиши в комментарии что именно не работает: ОС, команда запуска, текст ошибки. Разберёмся.