LEARN X · ЗА 15 МИН

Bash

Bash за 15 минут: переменные, аргументы, условия, циклы, case, функции, массивы, редиректы, строки и trap — весь язык скриптов в комментированном коде.

Bash — язык скриптов командной оболочки Unix. Здесь весь язык на одной странице: синтаксис в плотно закомментированном коде, почти без прозы. Команды (grep, sed, find) живут в отдельной шпаргалке — тут только сам ЯЗЫК.

1. Шебанг и запуск

#!/bin/bash
# Первая строка — шебанг: указывает интерпретатор скрипта.
# Запуск:
#   chmod +x script.sh   # сделать файл исполняемым
#   ./script.sh          # запустить
#   bash script.sh       # или явно через bash (chmod не нужен)

# Это однострочный комментарий — начинается с #.

echo "Привет, мир"        # вывод строки + перевод строки -> Привет, мир
echo -n "без переноса"    # -n: не добавлять \n в конце
echo -e "таб:\tстрока"   # -e: включить escape-последовательности (\t \n)
printf "%s = %d\n" "x" 42 # printf: форматированный вывод -> x = 42

2. Переменные

name="Аня"          # присваивание БЕЗ пробелов вокруг = (name = ... — ошибка!)
count=10            # числа хранятся как строки
echo "$name"        # обращение через $ -> Аня
echo "${name}!"     # ${} — явные границы имени -> Аня!
echo "${name}_id"   # без скобок $name_id искал бы переменную name_id

# Кавычки решают всё:
echo "$name стоит 5$"   # двойные: подстановка работает -> Аня стоит 5$
echo '$name дословно'   # одинарные: НЕТ подстановки -> $name дословно

readonly PI=3.14        # константа, переприсвоить нельзя
unset count             # удалить переменную
export PATH_EXTRA=/opt  # export — передать переменную дочерним процессам

# Значения по умолчанию:
echo "${city:-Москва}"  # если city пуста/не задана -> Москва
echo "${city:=Питер}"   # то же, но ещё и присвоит city=Питер
echo "${must:?нужно!}"  # если must пуста — выйти с ошибкой "нужно!"

3. Аргументы и спецпеременные

#!/bin/bash
# Запуск: ./script.sh apple banana

echo "$0"    # имя самого скрипта -> ./script.sh
echo "$1"    # первый аргумент -> apple
echo "$2"    # второй аргумент  -> banana
echo "$#"    # КОЛИЧЕСТВО аргументов -> 2
echo "$@"    # ВСЕ аргументы списком -> apple banana
echo "$*"    # все аргументы одной строкой
echo "$$"    # PID текущего процесса

false        # команда, завершившаяся с ошибкой
echo "$?"    # код возврата ПОСЛЕДНЕЙ команды -> 1 (0 = успех, иначе ошибка)

shift        # сдвиг: $2 становится $1, $3 -> $2 и т.д.

# Перебор всех аргументов:
for arg in "$@"; do
  echo "аргумент: $arg"
done

4. Подстановки

# Подстановка команды — вставить вывод команды в строку:
now=$(date +%H:%M)        # современный синтаксис $( )
files=`ls`                # устаревший синтаксис с обратными кавычками
echo "Сейчас $now"

# Арифметика — внутри $(( )):
echo $(( 2 + 3 * 4 ))     # -> 14
echo $(( 10 / 3 ))       # целочисленное деление -> 3
echo $(( 10 % 3 ))       # остаток -> 1
echo $(( 2 ** 8 ))       # степень -> 256
n=5; (( n++ ))            # (( )) — арифметический контекст, n стало 6
echo $n

# Расширение скобок (brace expansion):
echo {1..5}              # -> 1 2 3 4 5
echo {a..e}              # -> a b c d e
echo {0..10..2}         # шаг 2 -> 0 2 4 6 8 10
echo file_{jpg,png,gif}  # -> file_jpg file_png file_gif
echo img{01..03}.png     # -> img01.png img02.png img03.png

5. Условия

Между [ и операндами ОБЯЗАТЕЛЬНЫ пробелы. [[ ]] — расширенная версия Bash: безопаснее и умеет &&, ||, шаблоны.

x=10
if [ "$x" -gt 5 ]; then
  echo "больше пяти"
elif [ "$x" -eq 5 ]; then
  echo "ровно пять"
else
  echo "меньше пяти"
fi

# Числовые сравнения (внутри [ ] и test):
#   -eq равно   -ne не равно   -gt >   -lt <   -ge >=   -le <=

# Строковые сравнения:
if [ "$name" = "Аня" ]; then echo "совпало"; fi   # = равно, != не равно
if [ -z "$s" ]; then echo "строка пустая"; fi      # -z пустая
if [ -n "$s" ]; then echo "строка непустая"; fi    # -n непустая

# Проверки файлов:
#   -e существует   -f файл   -d каталог   -r читаемый   -w записываемый
if [ -f "/etc/hosts" ]; then echo "файл есть"; fi

# [[ ]] — расширенный тест Bash:
if [[ "$x" -gt 5 && "$name" == А* ]]; then   # == с шаблоном, логика внутри
  echo "оба условия верны"
fi
if [[ "$file" =~ \.txt$ ]]; then echo "это .txt (regex)"; fi  # =~ regex

6. Циклы

# for по списку:
for fruit in apple banana cherry; do
  echo "$fruit"
done

# for по диапазону:
for i in {1..3}; do echo "шаг $i"; done   # шаг 1, шаг 2, шаг 3

# for в C-стиле:
for (( i = 0; i < 3; i++ )); do echo "i=$i"; done

# while — пока условие истинно:
n=1
while [ "$n" -le 3 ]; do
  echo "n=$n"
  (( n++ ))
done

# until — пока условие ЛОЖНО (зеркало while):
m=1
until [ "$m" -gt 3 ]; do
  echo "m=$m"
  (( m++ ))
done

# Управление циклом:
for i in {1..5}; do
  (( i == 2 )) && continue   # continue: пропустить текущую итерацию
  (( i == 4 )) && break      # break: прервать весь цикл
  echo "$i"                  # выведет 1 3
done

7. Case

read -p "Команда: " cmd
case "$cmd" in
  start)
    echo "запуск"
    ;;                      # ;; завершает ветку
  stop|halt)                # | — несколько шаблонов в одной ветке
    echo "остановка"
    ;;
  re*)                      # шаблон: всё, что начинается с re
    echo "перезапуск?"
    ;;
  *)                        # * — значение по умолчанию (любое другое)
    echo "неизвестно: $cmd"
    ;;
esac                        # case читается задом наперёд: esac

8. Функции

# Два способа объявить:
greet() {                  # классический, скобки пустые
  echo "Привет, $1"        # аргументы — как у скрипта: $1 $2 $@ $#
}
function bye {             # с ключевым словом function
  echo "Пока, $1"
}

greet "Аня"                # вызов БЕЗ скобок -> Привет, Аня
bye "Аня"                  # -> Пока, Аня

# return возвращает только КОД (0..255), не данные:
is_even() {
  (( $1 % 2 == 0 )) && return 0 || return 1
}
if is_even 4; then echo "чётное"; fi

# Чтобы "вернуть" значение — печатают его и ловят через $( ):
square() { echo $(( $1 * $1 )); }
result=$(square 5)         # -> 25
echo "$result"

# local — локальная переменная (видна только внутри функции):
counter() {
  local n=0               # без local n утёк бы в глобальную область
  (( n++ ))
  echo "$n"
}

9. Массивы

# Индексируемый массив:
fruits=(apple banana cherry)   # объявление через скобки
fruits[3]="date"               # добавить/изменить элемент

echo "${fruits[0]}"            # доступ по индексу -> apple
echo "${fruits[@]}"           # все элементы -> apple banana cherry date
echo "${#fruits[@]}"          # количество элементов -> 4
echo "${!fruits[@]}"          # все индексы -> 0 1 2 3

# Перебор:
for f in "${fruits[@]}"; do
  echo "$f"
done

fruits+=("elderberry")        # добавить в конец
unset 'fruits[1]'             # удалить элемент по индексу

# Ассоциативный массив (словарь), Bash 4+:
declare -A ages              # declare -A — обязательно для словаря
ages["Аня"]=30
ages["Боб"]=25
echo "${ages[Аня]}"          # -> 30
for key in "${!ages[@]}"; do  # ${!arr[@]} — перебор ключей
  echo "$key: ${ages[$key]}"
done

10. Ввод-вывод и редиректы

# Чтение со стандартного ввода:
read -p "Имя: " user          # -p печатает приглашение
read -s -p "Пароль: " pass    # -s скрывает ввод (для паролей)
read -a arr                   # -a прочитать слова в массив

# Редиректы вывода:
echo "строка" > out.txt       # >  перезаписать файл
echo "ещё"    >> out.txt      # >> дописать в конец
wc -l < out.txt               # <  взять ввод из файла

# Потоки: 1 = stdout (вывод), 2 = stderr (ошибки):
ls /нет 2> err.txt            # 2> перенаправить только ошибки
ls /нет > all.txt 2>&1        # 2>&1 слить stderr в stdout (порядок важен!)
command > /dev/null 2>&1      # выбросить весь вывод в никуда

# Конвейер (pipe) — вывод одной команды на ввод другой:
cat out.txt | sort | uniq    # | связывает stdout -> stdin

# Here-document — многострочный текст на вход:
cat <<EOF
Строка 1, переменные работают: $user
Строка 2
EOF
cat <<'EOF'                   # кавычки у метки — без подстановки $
Дословно $user
EOF

# Here-string — короткая строка на вход:
grep "a" <<< "banana"

11. Строки

s="Hello, World"

echo "${#s}"            # длина строки -> 12
echo "${s:7}"          # подстрока с 7-го символа -> World
echo "${s:0:5}"        # 5 символов с начала -> Hello

# Замена:
echo "${s/o/0}"        # заменить ПЕРВОЕ вхождение -> Hell0, World
echo "${s//o/0}"       # заменить ВСЕ вхождения  -> Hell0, W0rld

# Обрезка по шаблону (удаление префикса/суффикса):
path="/home/user/file.txt"
echo "${path##*/}"      # ## — самый длинный префикс по */ -> file.txt
echo "${path%/*}"      # %  — самый короткий суффикс /* -> /home/user
echo "${path%.txt}"    # убрать суффикс .txt -> /home/user/file

# Регистр (Bash 4+):
echo "${s^^}"          # ВЕРХНИЙ регистр -> HELLO, WORLD
echo "${s,,}"          # нижний регистр  -> hello, world

# Проверка подстроки:
if [[ "$s" == *World* ]]; then echo "содержит World"; fi

12. Полезное

#!/bin/bash
set -e            # выйти при первой же ошибке (ненулевой код любой команды)
set -u            # ошибка при использовании необъявленной переменной
set -o pipefail   # код ошибки конвейера = код упавшего звена, а не последнего
set -euo pipefail # всё сразу — типичная "строгая" преамбула скрипта

# Логические операторы по коду возврата:
mkdir -p /tmp/x && echo "ок"      # && — следующая команда, если ПРЕДЫДУЩАЯ успешна
cat нет.txt || echo "не нашёл"     # || — следующая, если предыдущая УПАЛА
test -d /tmp && echo "есть" || echo "нет"  # тернарная идиома

# trap — перехват сигналов и событий (очистка ресурсов):
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT          # выполнить при выходе из скрипта (любом)
trap 'echo прервано; exit 1' INT  # перехват Ctrl+C (сигнал INT)

# Коды выхода:
exit 0            # 0 — успех
exit 1            # 1..255 — ошибка (1 общая, 2 неверное использование, ...)

# Полезные идиомы:
: "${VAR:?переменная VAR обязательна}"   # упасть, если VAR не задана
command -v git >/dev/null || { echo "нет git"; exit 1; }  # проверка наличия
Поддержать проект