Skip to content

Bash Шпаргалка

Командная оболочка Unix и язык команд для скриптинга и автоматизации.

01

Переменные и строки

Переменные и присваивание

Переменные Bash по умолчанию являются нетипизированными строками; арифметика работает, если значение числовое. НИКОГДА не ставьте пробелы вокруг = в присваиваниях — 'name = Alice' пытается запустить команду с именем name. Используйте $(...) для подстановки команд (предпочтительнее обратных кавычек). export делает переменную доступной дочерним процессам; readonly делает её неизменяемой. Переменные окружения (экспортированные) наследуются скриптами и программами, которые вы запускаете.

bash
# no spaces around = in assignments
name="Alice"
age=30
PI=3.14
active=true

# use variables with $ prefix
echo "Hello, $name!"
echo "Age: $age"

# command substitution: capture command output
today=$(date +%Y-%m-%d)
files=$(ls | wc -l)
echo "Today is $today, $files files"

# readonly and environment variables
readonly MAX=100
export PATH="$PATH:/opt/bin"
env | grep PATH

Подстановка параметров строк

Подстановка параметров — одна из самых мощных возможностей Bash, заключается в ${...}. ${#var} возвращает длину; ${var:offset:length} извлекает подстроки (отрицательный offset отсчитывается с конца — обратите внимание на пробел перед -). ,, и ^^ преобразуют регистр (Bash 4+). :- предоставляет значения по умолчанию без установки переменной; := устанавливает её как побочный эффект; :? прерывает выполнение с ошибкой — отлично подходит для проверки обязательных аргументов в скриптах.

bash
s="Hello World"
echo ${#s}                # length: 11
echo ${s:0:5}             # substring: Hello
echo ${s:6}               # from index 6: World
echo ${s: -5}             # last 5 chars: World

# case conversion (Bash 4+)
echo ${s,,}               # lowercase: hello world
echo ${s^^}               # uppercase: HELLO WORLD
echo ${s^l}               # capitalize first if 'l': Hello World

# default values
echo ${name:-Guest}       # use Guest if name unset
echo ${name:=Guest}       # assign Guest if unset, then use
echo ${name:?required}    # error and exit if unset

Поиск и замена в строках

Шаблон / заменяет; // заменяет все вхождения; # совпадает в начале; % совпадает в конце. ## и %% являются жадными (самое длинное совпадение), тогда как # и % — нежадными. Это необходимо для работы с путями: ${path##*/} извлекает basename (как команда basename), а ${path%/*} извлекает каталог (как dirname). Освоение этого позволяет избежать множества вызовов внешних команд вроде sed и cut.

bash
s="Hello World Hello"

# replace first occurrence
echo ${s/Hello/Hi}        # Hi World Hello

# replace all occurrences
echo ${s//Hello/Hi}       # Hi World Hi

# replace at beginning / end
echo ${s/#Hello/Hi}       # Hi World Hello (prefix)
echo ${s/%Hello/Hi}       # Hello World Hello (no suffix match)

# delete patterns (replace with empty)
echo ${s//Hello/}         #  World 
path="/usr/local/bin/app"
echo ${path##*/}          # app (longest prefix)
echo ${path%/*}           # /usr/local/bin (shortest suffix)

# extract filename and extension
file="report.tar.gz"
echo ${file%.*}           # report.tar (remove ext)
echo ${file##*.}          # gz (keep ext)

Кавычки и специальные символы

Одинарные кавычки сохраняют всё буквально — никакого раскрытия переменных, никакой обработки escape-последовательностей (вы даже не можете включить одинарную кавычку внутрь одинарных кавычек). Двойные кавычки допускают раскрытие $, обратных кавычек и \, сохраняя пробелы и специальные символы — почти всегда предпочтительно заключать переменные в двойные кавычки, чтобы предотвратить ошибки разбиения на слова и globbing. Используйте $'...' для ANSI-C escape-последовательностей вроде \n и \t.

bash
# single quotes: literal, no expansion
echo 'Hello $USER'         # Hello $USER

# double quotes: expansion happens
echo "Hello $USER"         # Hello alice

# escape special chars with backslash
echo "Price: \\$5.00"      # Price: $5.00
echo 'It\'s a test'       # It's a test (escape inside single)

# concatenate strings
a="Hello"
b="World"
c="$a, $b!"
echo $c                    # Hello, World!

# multi-line string
msg="Line 1
Line 2
Line 3"
echo "$msg"

Чтение и пользовательский ввод

read считывает пользовательский ввод. -p выводит приглашение, -t устанавливает тайм-аут (возвращает ненулевое значение), -a считывает в массив. При построчном чтении файлов всегда используйте 'IFS= read -r' — IFS= сохраняет начальные/конечные пробелы, а -r отключает escape-обработку обратной косой черты, так что обратные слэши сохраняются буквально. Это канонический безопасный шаблон для построчной обработки файлов.

bash
# read a single value
echo -n "Enter your name: "
read name
echo "Hi, $name!"

# read multiple values
read -p "First and last: " first last

# read with timeout (seconds)
read -t 5 -p "Quick! " answer || echo "too slow"

# read into an array
echo "Enter colors:"
read -a colors
echo "First: ${colors[0]}"

# read line by line from a file
while IFS= read -r line; do
    echo ">>> $line"
done < file.txt
02

Массивы

Индексные массивы

Массивы Bash индексируются с нуля. @ и * раскрываются во все элементы — всегда берите их в кавычки ("${arr[@]}") для корректной обработки элементов с пробелами. ${#arr[@]} возвращает количество элементов. += добавляет в конец. unset оставляет промежуток в индексах; для переиндексации используйте arr=("${arr[@]}"). Срез ${arr[@]:start:count} возвращает подмножество.

bash
# create and populate
fruits=("apple" "banana" "cherry")
fruits[3]="date"

# access elements
echo ${fruits[0]}         # apple
echo ${fruits[@]}         # all: apple banana cherry date
echo ${fruits[*]}         # same as @ in most contexts

# count and slice
echo ${#fruits[@]}        # count: 4
echo ${fruits[@]:1:2}     # elements 1-2: banana cherry

# append
fruits+=("elderberry")

# iterate
for f in "${fruits[@]}"; do
    echo "- $f"
done

# delete an element
unset fruits[1]            # removes "banana" (gap remains)

Ассоциативные массивы (map)

Ассоциативные массивы (declare -A) отображают строковые ключи на значения, подобно словарям в других языках — доступны в Bash 4+. Используйте ${!arr[@]} для получения ключей и ${arr[@]} для значений. Проверка -v определяет, существует ли ключ. В отличие от индексных массивов, ключи — произвольные строки, поэтому заключение ключей с пробелами в кавычки обязательно. Требуют Bash 4+ (в macOS по умолчанию поставляется Bash 3).

bash
# must declare before use (Bash 4+)
declare -A ages

# key-value assignment
ages[alice]=30
ages[bob]=25
ages["carol smith"]=28

# access by key
echo ${ages[alice]}       # 30
echo ${ages[bob]}         # 25

# all keys and values
echo ${!ages[@]}          # keys: alice bob carol smith
echo ${ages[@]}           # values: 30 25 28
echo ${#ages[@]}          # count: 3

# iterate keys
for name in "${!ages[@]}"; do
    echo "$name is ${ages[$name]}"
done

# check if key exists
if [[ -v ages[alice] ]]; then
    echo "alice exists"
fi

Шаблоны итерации массивов

Существует три основных стиля итерации: по значению (for x in "${arr[@]}"), по индексу (for i in "${!arr[@]}") и C-стиль. Форма по индексу полезна, когда нужна позиция. Всегда заключайте "${arr[@]}" в кавычки, чтобы элементы, содержащие пробелы, сохранялись как отдельные элементы. Построение массивов с помощью += в цикле — идиоматичный способ накопления результатов.

bash
arr=(one two three four five)

# iterate by value
for item in "${arr[@]}"; do
    echo "$item"
done

# iterate by index
for i in "${!arr[@]}"; do
    echo "[$i] = ${arr[$i]}"
done

# C-style indexed loop
for ((i=0; i<${#arr[@]}; i++)); do
    echo "${arr[$i]}"
done

# map/transform: build a new array
upper=()
for w in "${arr[@]}"; do
    upper+=("$(echo $w | tr a-z A-Z)")
done
echo "${upper[@]}"

Массив из вывода команды

mapfile/readarray (Bash 4+) считывает строки в массив за один приём — намного быстрее, чем цикл while для больших файлов. Чтобы разделить строку с разделителями, установите IFS и используйте read -ra. Для имён файлов (которые могут содержать пробелы или переводы строк) всегда используйте find -print0 с read -d '' для разбиения по нулевым байтам — единственный безопасный разделитель для имён файлов. Подстановка процесса < <(...) передаёт данные в цикл без порождения subshell.

bash
# split a string into an array
csv="a,b,c,d"
IFS=',' read -ra parts <<< "$csv"
echo ${parts[2]}          # c

# read lines of a file into an array
mapfile -t lines < file.txt
echo "${lines[0]}"        # first line
echo "${#lines[@]}"       # line count

# alternative: readarray (same as mapfile)
readarray -t lines2 < file.txt

# from command output (words split on IFS)
files=($(ls *.txt))
echo "${files[@]}"

# safer: read null-delimited output
while IFS= read -r -d '' f; do
    files+=("$f")
done < <(find . -name "*.txt" -print0)

Специальные переменные

Эти специальные переменные необходимы для скриптинга. $0 — путь к скрипту; $1-$9 — позиционные аргументы (используйте ${10} для двух цифр). Всегда заключайте "$@" в кавычки, чтобы сохранить аргументы с пробелами — "$*" объединяет их в одну строку. $? возвращает статус завершения (0 = успех) и является основой всех проверок ошибок. $$ и $! полезны для PID-файлов и управления процессами. shift отбрасывает $1 и сдвигает остальные вниз.

bash
# positional parameters
echo $0       # script name
echo $1       # first argument
echo $#       # argument count
echo $@       # all arguments (separate words)
echo "$@"     # all arguments (preserves quoting)
echo $*       # all arguments (single string)

# process and shell info
echo $$       # current shell PID
echo $!       # PID of last background command
echo $?       # exit status of last command (0 = success)
echo $-       # current shell option flags
echo $_       # last argument of previous command

# shift arguments
echo "$1 $2"  # a b
shift
echo "$1 $2"  # b c

# iterate all args safely
for arg in "$@"; do
    echo "arg: $arg"
done
03

Поток управления и проверки

If / Elif / Else

[ ] — команда test стандарта POSIX (переносима, но ограничена); [[ ]] — расширенная версия Bash, поддерживающая сопоставление шаблонов (== с подстановочными знаками), regex (=~), операторы && / || и не требующая кавычек для переменных. В скриптах Bash предпочитайте [[ ]]. Всегда заключайте переменные в кавычки в [ ], чтобы избежать синтаксических ошибок с пустыми значениями или значениями, содержащими пробелы. Проверки -f, -r, -d, -e проверяют существование и атрибуты файлов.

bash
# basic if
if [ "$age" -ge 18 ]; then
    echo "adult"
elif [ "$age" -ge 13 ]; then
    echo "teen"
else
    echo "child"
fi

# modern [[ ]] test (Bash-specific, safer)
if [[ $name == "Alice" ]]; then
    echo "hi Alice"
fi

# pattern matching in [[ ]]
if [[ $file == *.txt ]]; then
    echo "text file"
fi

# regex matching
if [[ $email =~ ^[a-z]+@[a-z]+.[a-z]+$ ]]; then
    echo "valid email"
fi

# multiple conditions
if [[ -f file.txt && -r file.txt ]]; then
    echo "readable file"
fi

Операторы проверки

Файловые проверки (-f, -d, -r и т. д.) проверяют атрибуты файловой системы. Строковые проверки используют = и != (или == в [[ ]]); -z проверяет пустоту, -n — непустоту. Для целочисленных сравнений используются -eq, -ne, -lt, -gt, -le, -ge — НЕ используйте < > для целых чисел в [ ] (это перенаправление!). В [[ ]] и (( )) можно использовать привычные операторы < > <= >=. Запоминание этих операторов необходимо для написания корректных условных конструкций.

bash
# file tests
[ -f file ]       # exists and is regular file
[ -d dir ]        # exists and is directory
[ -e path ]       # exists (any type)
[ -r file ]       # readable
[ -w file ]       # writable
[ -x file ]       # executable
[ -s file ]       # exists and is non-empty
[ file1 -nt file2 ]  # newer than
[ file1 -ot file2 ]  # older than

# string tests
[ -z "$s" ]       # empty string
[ -n "$s" ]       # non-empty string
[ "$a" = "$b" ]   # equal (use == in [[ ]])
[ "$a" != "$b" ]  # not equal

# integer tests
[ $n -eq 5 ]      # equal
[ $n -ne 5 ]      # not equal
[ $n -lt 5 ]      # less than
[ $n -gt 5 ]      # greater than
[ $n -le 5 ]      # less or equal
[ $n -ge 5 ]      # greater or equal

Оператор case

case — эквивалент switch в Bash — сопоставляет значение с glob-шаблонами. | разделяет альтернативы. ;; завершает ветвь (как break). Шаблон *) — значение по умолчанию. Шаблоны поддерживают подстановочные знаки (*, ?, [abc]), но не полный regex. case чище, чем длинные цепочки if-elif для диспетчеризации по одному значению, и это стандартный способ создания подкоманд командной строки (start/stop/restart).

bash
case $1 in
    start)
        echo "Starting service..."
        systemctl start app
        ;;
    stop)
        echo "Stopping service..."
        systemctl stop app
        ;;
    restart)
        $0 stop
        $0 start
        ;;
    status)
        systemctl status app
        ;;
    --help|-h)
        echo "Usage: $0 {start|stop|restart|status}"
        ;;
    *)
        echo "Unknown command: $1" >&2
        exit 1
        ;;
esac

# pattern matching in case
case $file in
    *.jpg|*.png)    echo "image" ;;
    *.mp3|*.wav)    echo "audio" ;;
    *.txt)          echo "text" ;;
    *)              echo "other" ;;
esac

Арифметика и (( ))

$(( )) вычисляет арифметические выражения и возвращает результат. Внутри (( )) именам переменных не нужен $ — просто используйте имя. (( )) как команда возвращает статус завершения 0 (истина), если результат не равен нулю, что идеально для числовых условий. Bash выполняет только целочисленную арифметику; для чисел с плавающей точкой используйте bc или awk. Операторы: + - * / % ** и C-стиль операторов сравнения и побитовых операций работают.

bash
# arithmetic expansion
x=5
y=$((x + 3))
echo $y              # 8
echo $((x * 2))      # 10
echo $((2 ** 10))    # 1024 (exponent)
echo $((17 % 5))     # 2 (modulo)
echo $((17 / 5))     # 3 (integer division)

# (( )) condition: no $ needed, returns exit status
x=10
if (( x > 5 && x < 20 )); then
    echo "in range"
fi

# increment / decrement
((x++))             # post-increment
((x--))             # post-decrement
((++x))             # pre-increment

# bitwise operators
echo $((5 & 3))     # 1 (AND)
echo $((5 | 2))     # 7 (OR)
echo $((5 << 1))    # 10 (left shift)

Сокращённое вычисление и тернарный оператор

&& и || — операторы сокращённого вычисления, которые также служат лаконичным потоком управления: 'cmd1 && cmd2' запускает cmd2, только если cmd1 завершилась успешно; 'cmd1 || cmd2' запускает cmd2, только если cmd1 завершилась неудачей. Это заменяет простые if-else в одну строку. Шаблон 'A && B || C' имитирует тернарный оператор, но содержит тонкий баг, если B может завершиться неудачей — для надёжного кода используйте настоящий оператор if. Двоеточие (:) — команда без действия, полезная для пустых ветвей.

bash
# && and || as short-circuit control flow
[[ -f file.txt ]] && echo "exists"
[[ -f file.txt ]] || echo "missing"

# combined: do this OR fail
mkdir -p build && cd build && make || exit 1

# ternary-like (Bash doesn't have a real ternary)
result=$([[ $x -gt 0 ]] && echo "positive" || echo "non-positive")

# default value via &&
echo ${name:-"anonymous"}

# run command, fallback on failure
ping -c1 host && echo "up" || echo "down"

# null command as no-op
if condition; then
    :    # do nothing
fi
04

Циклы и итерация

Циклы for

Цикл for перебирает список слов. Раскрытие фигурных скобок {1..5} генерирует последовательности (с необязательным шагом {start..end..step}). C-стиль for ((init; cond; update)) лучше всего подходит для числовых счётчиков. При переборе файлов с *.txt некотируемые glob раскрываются безопасно, но если файлов нет, вы получите литерал '*.txt' — включите shopt -s nullglob, чтобы получить пустой список.

bash
# iterate a list
for fruit in apple banana cherry; do
    echo "$fruit"
done

# iterate an array
colors=("red" "green" "blue")
for c in "${colors[@]}"; do
    echo "Color: $c"
done

# range with brace expansion
for i in {1..5}; do
    echo "Number: $i"
done

# range with step
for i in {0..20..2}; do echo $i; done    # 0 2 4 ... 20

# C-style for loop
for ((i=0; i<5; i++)); do
    echo "Iteration $i"
done

# iterate command output
for file in *.txt; do
    echo "Processing $file"
done

Циклы while и until

while выполняется, пока условие выполняется (exit 0); until — пока оно не станет истинным — они противоположны. Шаблон 'while read' — канонический способ построчной обработки файлов; всегда используйте 'IFS= read -r' для безопасности. Обратите внимание, что конвейер в цикл while запускает его в subshell, поэтому изменения переменных не сохраняются — используйте подстановку процесса '< <(cmd)', когда нужно сохранить переменные.

bash
# while: runs while condition is true
count=0
while [ $count -lt 5 ]; do
    echo "Count: $count"
    ((count++))
done

# read lines from a file
while IFS= read -r line; do
    echo "Line: $line"
done < input.txt

# infinite loop with break
while true; do
    read -p "Quit? (y/n) " ans
    [[ $ans == "y" ]] && break
done

# until: runs until condition is true (opposite of while)
n=0
until [ $n -ge 3 ]; do
    echo "n=$n"
    ((n++))
done

# while reading from a pipeline
seq 1 5 | while read n; do
    echo "got $n"
done

Break, Continue и select

break выходит из цикла (break N выходит из N вложенных циклов); continue переходит к следующей итерации. select создаёт интерактивное нумерованное меню из списка — цикл выполняется до прерывания. Эти операторы управления работают в циклах for, while и until. Форма break N необходима для выхода из глубоко вложенных циклов, но используйте её умеренно, так как это может усложнить чтение кода.

bash
# break exits the loop
for i in {1..10}; do
    [[ $i -eq 5 ]] && break
    echo $i       # prints 1 2 3 4
done

# continue skips to next iteration
for i in {1..6}; do
    (( i % 2 == 0 )) && continue
    echo $i       # prints 1 3 5 (odd only)
done

# break out of nested loops
for i in 1 2 3; do
    for j in a b c; do
        [[ $j == "b" ]] && break 2   # break both loops
        echo "$i$j"
    done
done

# select menu
select opt in "Start" "Stop" "Quit"; do
    case $opt in
        Start) echo "starting" ;;
        Stop)  echo "stopping" ;;
        Quit)  break ;;
        *)     echo "invalid" ;;
    esac
done

Безопасный перебор файлов

Разбор вывода ls — классический баг — имена файлов с пробелами, переводами строк или специальными символами ломают его. Используйте glob оболочки (*.txt) непосредственно в циклах for и заключайте переменную в кавычки. Для рекурсивного или сложного поиска используйте find -print0, передаваемый по конвейеру в 'read -d ''' — нулевые байты единственный безопасный разделитель для имён файлов. Включите nullglob, чтобы нерасширенные glob давали пустой список, а не литеральный шаблон.

bash
# BAD: breaks on filenames with spaces
for f in $(ls *.txt); do
    cat "$f"
done

# GOOD: glob expands safely
for f in *.txt; do
    [[ -f "$f" ]] || continue
    cat "$f"
done

# BEST: handle spaces, newlines, and no matches
shopt -s nullglob dotglob
for f in *.txt; do
    echo "Processing: $f"
done

# find with -print0 and while read -d ''
while IFS= read -r -d '' f; do
    echo "Found: $f"
done < <(find . -type f -name "*.txt" -print0)

# process files modified today
for f in $(find . -mtime -1 -print); do
    echo "Recent: $f"
done

Диапазоны и генерация последовательностей

Раскрытие фигурных скобок {a..b} генерирует последовательности на этапе разбора — быстро и встроено. Поддерживает буквы, цифры, заполнение нулями и шаги ({start..end..step}). seq — внешняя команда с большим количеством опций форматирования (-f для стиля printf). Раскрытие скобок также создаёт несколько аргументов: echo file{1..3}.{txt,log} генерирует 6 имён файлов. Для простых диапазонов предпочитайте раскрытие скобок; используйте seq, когда нужно кастомное форматирование.

bash
# brace expansion (not a loop, but generates lists)
echo {1..10}                 # 1 2 3 4 5 6 7 8 9 10
echo {a..e}                  # a b c d e
echo {01..10}                # zero-padded: 01 02 ... 10
echo file{1..3}.txt          # file1.txt file2.txt file3.txt

# seq command
seq 1 5                      # 1 2 3 4 5
seq 1 2 10                   # 1 3 5 7 9 (step 2)
seq -f "%03d" 1 5            # 001 002 003 004 005

# use seq in a for loop
for i in $(seq 1 5); do
    echo $i
done

# generate a list of dates
for d in $(seq -f "2024-01-%02g" 1 31); do
    echo "$d"
done
05

Функции

Определение и вызов функций

Функции в Bash определяются как name() или function name. Вызывайте их по имени с аргументами, разделёнными пробелами (без скобок). ВСЕГДА используйте 'local' для переменных внутри функций, чтобы избежать загрязнения глобальной области видимости — без него присваивания утекают наружу. Функции Bash не могут возвращать значения напрямую (return — это статус завершения 0-255); чтобы вернуть строку, выведите её через echo и захватите через $().

bash
# two equivalent syntaxes
greet() {
    echo "Hello, $1!"
}
function greet2() {
    echo "Hi, $1!"
}

# call with arguments (no parens)
greet "Alice"               # Hello, Alice!
greet2 "Bob"                # Hi, Bob!

# local variables (function-scoped)
counter() {
    local count=0
    ((count++))
    echo $count
}

# return a value via echo + command substitution
add() {
    echo $(($1 + $2))
}
result=$(add 3 4)           # 7

Аргументы и параметры

Внутри функции $1, $@, $# ссылаются на аргументы функции, а не скрипта — они затеняют параметры уровня скрипта. $FUNCNAME содержит имя функции (полезно для отладки). shift внутри функции влияет только на параметры функции. Чтобы передать аргументы скрипта в функцию, используйте func "$@". Это затенение — причина, по которой функции являются переиспользуемыми строительными блоками.

bash
# all positional params work inside functions
show_args() {
    echo "Function name: $FUNCNAME"
    echo "First arg:  $1"
    echo "Second arg: $2"
    echo "All args:   $@"
    echo "Arg count:  $#"
}

show_args a b c
# Function name: show_args
# First arg:  a
# Second arg: b
# All args:   a b c
# Arg count:  3

# pass all script args to a function
process "$@"

# shift within a function (local to it)
parse() {
    while (($#)); do
        echo "arg: $1"
        shift
    done
}

Возвращаемые значения и статус завершения

Команда 'return' в Bash только устанавливает статус завершения (0-255), поэтому функции двойным образом служат логическими проверками. Чтобы вернуть строку, выведите её через echo и захватите через $(). Для нескольких значений используйте nameref (local -n) — Bash 4.3+ — который позволяет функции присваивать значение переменной вызывающего по имени. Это самый чистый способ вернуть сложные данные. Избегайте использования глобальных переменных для возвращаемых значений, так как это делает функции непереиспользуемыми.

bash
# return sets exit status (0-255), not a value
is_even() {
    if (($1 % 2 == 0)); then
        return 0    # true/success
    else
        return 1    # false/failure
    fi
}

# use as a condition
if is_even 4; then
    echo "4 is even"
fi

# chain with && and ||
is_even 4 && echo "yes" || echo "no"

# return a string via echo (capture with $())
to_upper() {
    echo "${1^^}"
}
UP=$(to_upper hello)        # HELLO

# return multiple values via global or nameref
set_pair() {
    local -n _r=$1
    _r=(10 20)
}
set_pair result
echo "${result[@]}"       # 10 20

Область видимости переменных

Переменные в Bash по умолчанию глобальны — даже при присваивании внутри функции! Это частый источник багов. Всегда используйте 'local' для переменных, внутренних для функции. 'declare -g' явно создаёт глобальную переменную из функции. Рекурсия работает, но медленна в Bash (каждый вызов порождает subshell для $()); используйте её умеренно. Пример с факториалом демонстрирует шаблон: локальные переменные + рекурсивный вызов + арифметика.

bash
# global by default
x=1
modify() {
    x=2          # changes the GLOBAL x!
    local y=3    # local to function
    echo "inside: x=$x y=$y"
}

modify
echo "outside: x=$x"       # x=2 (modified!)
echo "outside: y=$y"       # empty (y is local)

# declare -g for global inside a function
set_global() {
    declare -g NEW_VAR=42
}
set_global
echo $NEW_VAR              # 42

# recursion with local
factorial() {
    local n=$1
    if (( n <= 1 )); then
        echo 1
    else
        local prev=$(factorial $((n - 1)))
        echo $((n * prev))
    fi
}
echo $(factorial 5)        # 120

Библиотеки и source

source (или .) выполняет файл в текущей оболочке, поэтому его функции и переменные становятся доступными — так вы строите переиспользуемые библиотеки. Распространённый шаблон — utils.sh со вспомогательными функциями, которые скрипты подключают через source. В отличие от выполнения скрипта (который запускается в subshell), код, подключённый через source, может изменять окружение вызывающего. Так же работают .bashrc и .bash_profile — они подключаются при запуске оболочки.

bash
# utils.sh — a reusable library
#!/bin/bash
log() {
    echo "[$(date +%H:%M:%S)] $*" >&2
}

die() {
    log "ERROR: $*"
    exit 1
}

confirm() {
    read -p "$1 [y/N] " ans
    [[ $ans =~ ^[Yy]$ ]]
}

# main.sh — use the library
source ./utils.sh        # or . ./utils.sh

log "Starting script"
confirm "Continue?" || die "aborted"
log "Done"

# source vs execute
# source: runs in current shell (shares variables)
# ./script.sh: runs in a subshell (isolated)
06

Обработка текста

grep — поиск по шаблону

grep находит строки, соответствующие шаблону. -i игнорирует регистр, -v инвертирует, -n показывает номера строк, -r рекурсивно обходит, -E использует расширенные regex (+, |, {}). -A/-B/-C показывают контекстные строки вокруг совпадений — незаменимо для понимания ошибок. Используйте --include для фильтрации типов файлов при рекурсивном поиске. grep возвращает статус завершения 0, если совпадение найдено, что полезно в условиях: if grep -q pattern file; then...

bash
# basic search
grep "error" app.log
grep -i "error" app.log          # case-insensitive
grep -v "debug" app.log          # invert (non-matching lines)
grep -c "error" app.log          # count matches
grep -n "error" app.log          # line numbers
grep -w "error" app.log          # whole word only

# recursive search
grep -rn "TODO" src/             # recursive + line numbers
grep -rl "TODO" src/             # files with matches only

# extended regex (or use egrep)
grep -E "error|warn|fatal" log
grep -E "^[0-9]{4}-" log         # lines starting with date

# context lines
grep -A 2 "error" log            # 2 lines after
grep -B 2 "error" log            # 2 lines before
grep -C 2 "error" log            # 2 lines before and after

# search multiple files
grep "function" *.js --include="*.js" -r .

sed — потоковый редактор

sed редактирует текстовые потоки неинтерактивно. Команда s выполняет подстановку; g делает её глобальной. -i редактирует по месту (сначала всегда тестируйте без -i!). Разделителем может быть любой символ — используйте | или # для путей, чтобы избежать экранирования слэшей. sed обрабатывает построчно, поэтому многострочные операции требуют команды N или hold space. Для сложных правок awk или perl могут быть нагляднее. Всегда заключайте скрипты sed в кавычки, чтобы предотвратить раскрытие оболочкой.

bash
# substitute (replace)
sed 's/old/new/' file.txt           # first occurrence per line
sed 's/old/new/g' file.txt          # all occurrences (global)
sed 's/old/new/3' file.txt          # 3rd occurrence only
sed 's/old/new/gi' file.txt         # global + case-insensitive

# in-place editing
sed -i 's/foo/bar/g' file.txt       # edit file in place
sed -i.bak 's/foo/bar/g' file.txt   # keep a backup

# delete lines
sed '/^#/d' file.txt                # delete comment lines
sed '/^$/d' file.txt                # delete blank lines
sed '5d' file.txt                   # delete line 5
sed '5,10d' file.txt                # delete lines 5-10

# print specific lines
sed -n '10,20p' file.txt            # print lines 10-20
sed -n '/pattern/p' file.txt        # print matching lines

# use a different delimiter for paths
sed 's|/usr/local|/opt|g' paths.txt

# multiple commands
sed -e 's/a/A/g' -e 's/b/B/g' file.txt

awk — обработка колонок

awk — мини-язык программирования для колоночных данных. $1, $2... — поля; $0 — вся строка; $NF — последнее поле. -F устанавливает входной разделитель; OFS — выходной. BEGIN выполняется до обработки, END — после. NR — номер записи (строки); NF — количество полей. awk идеален для обработки CSV/TSV, анализа логов и генерации отчётов — намного мощнее cut для чего-либо сложнее простого извлечения.

bash
# print columns (default separator: whitespace)
awk '{print $1}' file.txt           # first column
awk '{print $1, $3}' file.txt       # columns 1 and 3
awk '{print $NF}' file.txt          # last column
awk -F: '{print $1}' /etc/passwd    # split on :

# filter and print
awk '$3 > 100' file.txt             # lines where col 3 > 100
awk '$1 == "ERROR" {print $2}' log  # col 2 of ERROR lines
awk 'NR == 5' file.txt              # 5th line only
awk 'NR >= 10 && NR <= 20' file.txt # lines 10-20

# BEGIN and END blocks
awk 'BEGIN {print "Start"} {sum += $1} END {print sum}' nums.txt

# field separator and output
awk -F, 'BEGIN {OFS="|"} {print $1, $2}' csv.txt

# count lines matching a pattern
awk '/error/' file.txt              # like grep
awk '/error/ {count++} END {print count}' log

# built-in variables: NR (row), NF (fields), FS, OFS
awk '{print NR, NF, $0}' file.txt

cut, tr, sort и uniq

cut — простое извлечение полей/символов — быстрое, но ограниченное (нет поддержки кавычек). tr транслирует символы (отлично для смены регистра или замены разделителей), а -d удаляет. sort сортирует строки (-n численно, -r в обратном порядке, -k поле). uniq удаляет только СМЕЖНЫЕ дубликаты, поэтому всегда предварительно передавайте через sort. uniq -c с sort -rn — классический шаблон для частотного анализа: 'sort | uniq -c | sort -rn' показывает наиболее частые строки.

bash
# cut: extract fields or characters
cut -d: -f1 /etc/passwd            # field 1, delimiter :
cut -c1-5 file.txt                 # characters 1-5
cut -d, -f2,3 data.csv             # fields 2 and 3

# tr: translate or delete characters
echo "Hello" | tr 'a-z' 'A-Z'      # HELLO
echo "a,b,c" | tr ',' ' '          # a b c
echo "hello" | tr -d 'l'           # heo (delete)
tr -s ' ' < file.txt               # squeeze repeated spaces

# sort lines
sort file.txt                      # alphabetical
sort -n nums.txt                   # numeric
sort -rn nums.txt                  # reverse numeric
sort -t: -k3 -n /etc/passwd        # by 3rd field, numeric
sort -u file.txt                   # sort + unique

# uniq: filter or count adjacent duplicates
sort file.txt | uniq               # unique lines
sort file.txt | uniq -c            # count occurrences
sort file.txt | uniq -d            # only duplicates
sort file.txt | uniq -u            # only unique lines

Каналы и перенаправление

Каналы (pipes) соединяют stdout со stdin, создавая мощные конвейеры. > перенаправляет stdout (>> добавляет), 2> перенаправляет stderr, &> захватывает оба. Here-документы (<<EOF) передают многострочные строки; заключение разделителя в кавычки ('EOF') отключает раскрытие переменных. Подстановка процесса <(cmd) воспринимает вывод команды как временный файл — необходима для передачи в циклы while без subshell и для команд вроде diff, ожидающих файлы.

bash
# pipe: chain commands (stdout of one -> stdin of next)
cat file | grep "x" | sort | uniq -c | sort -rn | head

# redirect stdout
echo "hello" > file.txt            # overwrite
echo "world" >> file.txt           # append

# redirect stderr
command 2> error.log               # stderr to file
command 2>&1                       # stderr to stdout
command > all.log 2>&1             # both to same file
command &> all.log                 # both (Bash shortcut)

# stdin from file
command < input.txt

# here-string
grep "x" <<< "some text with x"

# here-document (multi-line input)
cat << EOF
Line 1
Line 2 with $VAR expansion
EOF

# quoted delimiter disables expansion
cat << 'EOF'
Literal $VAR no expansion
EOF

# process substitution
diff <(ls dir1) <(ls dir2)         # compare outputs
while read line; do echo "$line"; done < <(grep x file)
07

Операции с файлами и каталогами

find — поиск файлов

find — самый мощный инструмент поиска файлов. -name сопоставляет glob (используйте -iname без учёта регистра); -type фильтрует по f/d/l; -mtime/-mmin фильтруют по времени изменения (- = в течение, + = старше); -size фильтрует по размеру. -exec запускает команду для каждого результата; {} — имя файла, \; запускает для каждого файла, + объединяет в пакет. -delete безопаснее, чем -exec rm для удаления. Всегда тестируйте find без -delete сначала!

bash
# search by name
find . -name "*.js"
find . -iname "*.JS"               # case-insensitive
find / -name "*.conf" 2>/dev/null  # suppress permission errors

# search by type
find . -type f -name "*.txt"       # files only
find . -type d -name src           # directories only
find . -type l                     # symlinks

# search by time and size
find . -mtime -7                   # modified < 7 days ago
find . -mtime +30                  # modified > 30 days ago
find . -mmin -60                   # modified < 60 min ago
find . -size +10M                  # larger than 10MB
find . -size -1k                   # smaller than 1KB
find . -empty                      # empty files/dirs

# act on results (-exec)
find . -name "*.log" -exec rm {} \;
find . -name "*.js" -exec wc -l {} +
find . -type f -exec chmod 644 {} \;

# safe deletion with -delete
find /tmp -type f -mtime +7 -delete

Права и владение

Права — три триплета: владелец, группа, остальные. Каждая цифра — r(4)+w(2)+x(1), поэтому 755 = rwxr-xr-x. chmod изменяет права; chown изменяет владельца/группу. Символическая нотация (u+x, g-w) понятнее для инкрементальных изменений. umask устанавливает права по умолчанию для новых файлов (вычитается из 666 для файлов, 777 для каталогов). Для веб-серверов стандарт — 755 для каталогов и 644 для файлов; никогда не используйте 777 в продакшене.

bash
# view permissions
ls -l file.txt
# -rw-r--r-- 1 user group 1024 Jan 1 10:00 file.txt
#  ^^^ ^^^ ^^^
#  owner group others

# chmod: change permissions
chmod 755 script.sh        # rwxr-xr-x
chmod 644 file.txt         # rw-r--r--
chmod +x script.sh         # add execute for all
chmod u+x,g-w file.txt     # symbolic: user +x, group -w
chmod -R 755 directory/    # recursive

# chown: change owner
chown alice file.txt
chown alice:developers file.txt
chown -R alice:developers project/

# umask: default permissions for new files
umask                      # show current (e.g. 022)
umask 077                  # new files: 600, dirs: 700

# numeric permission reference:
# 7=rwx 6=rw 5=rx 4=r 3=wx 2=w 1=x 0=none

Копирование, перемещение, удаление и ссылки

cp -r рекурсивно копирует каталоги; -i запрашивает перед перезаписью (безопаснее); -p сохраняет временные метки и права. mv — это и перемещение, и переименование. rm -rf опасен — удаляет рекурсивно без запроса; всегда дважды проверяйте путь. Жёсткие ссылки указывают на тот же inode (тот же файл, не могут пересекать файловые системы или ссылаться на каталоги); символьные ссылки — ссылки по пути (могут ссылаться на что угодно, но ломаются при перемещении цели). Используйте ln -s для символьных ссылок.

bash
# copy
cp file.txt backup.txt
cp -r src/ dest/            # recursive (directories)
cp -i file.txt dest/        # interactive (prompt before overwrite)
cp -u file.txt dest/        # update (only if newer)
cp -p file.txt dest/        # preserve attributes

# move / rename
mv old.txt new.txt
mv file.txt /other/dir/
mv -i file.txt dest/        # prompt before overwrite

# remove
rm file.txt
rm -r directory/            # recursive
rm -f file.txt              # force (no error if missing)
rm -rf node_modules/        # force + recursive (DANGER)

# links
ln target hardlink          # hard link (same inode)
ln -s target symlink        # symbolic link (path reference)
ln -sf target symlink       # force recreate symlink

# view link target
readlink symlink
readlink -f symlink         # canonical absolute path

Архивы и сжатие

tar объединяет файлы в один архив; gzip/bzip2/xz сжимают его. Флаги: c=create, x=extract, t=list, f=file, z=gzip, j=bzip2, J=xz, v=verbose. .tar.gz — стандарт Unix; .zip распространён в Windows. bzip2 сжимает лучше gzip, но медленнее; xz — лучше всех, но самый медленный. Используйте -C для распаковки в конкретный каталог. Флаг -k сохраняет оригинал при сжатии.

bash
# tar + gzip (most common)
tar -czf archive.tar.gz dir/      # create
tar -xzf archive.tar.gz           # extract
tar -xzf archive.tar.gz -C /opt/  # extract to /opt
tar -tzf archive.tar.gz           # list contents

# tar + bzip2 (better compression)
tar -cjf archive.tar.bz2 dir/
tar -xjf archive.tar.bz2

# zip / unzip
zip -r archive.zip dir/
unzip archive.zip
unzip archive.zip -d /target

# gzip / gunzip (single files)
gzip file.txt           # -> file.txt.gz
gunzip file.txt.gz      # -> file.txt
gzip -k file.txt        # keep original

# xz (best compression)
tar -cJf archive.tar.xz dir/
tar -xJf archive.tar.xz

# mnemonic: eXtract, Create, List, File, gZip, bZip2, xz

Содержимое файлов и сравнение

cat выводит весь файл; head/tail показывают концы; tail -f стримит обновления в реальном времени (незаменимо для логов). less — интерактивный пейджер с поиском (/) и навигацией. diff сравнивает файлы; -u создаёт унифицированный формат, используемый patch. comm сравнивает два отсортированных файла и показывает строки, уникальные для каждого, или общие — полезно для сравнения списков. Всегда сортируйте входные данные перед comm, так как он требует отсортированные файлы.

bash
# view file contents
cat file.txt               # entire file
head -n 20 file.txt        # first 20 lines
tail -n 20 file.txt        # last 20 lines
tail -f app.log            # follow (live updates)
less file.txt              # pager (q to quit, / to search)

# line/word/char count
wc file.txt                # lines words bytes
wc -l file.txt             # lines only
wc -w file.txt             # words only

# compare files
diff file1.txt file2.txt
diff -u file1.txt file2.txt    # unified format (for patches)
diff -r dir1/ dir2/            # recursive

# patch
diff -u old new > change.patch
patch < change.patch
patch -R < change.patch        # reverse

# find duplicate lines across files
comm -12 <(sort a) <(sort b)   # lines in both
comm -23 <(sort a) <(sort b)   # only in a
comm -13 <(sort a) <(sort b)   # only in b
08

Управление процессами и сигналы

Фоновые задачи и управление заданиями

Добавление & запускает команду в фоне, возвращая управление немедленно. jobs выводит активные задания; fg/bg перемещают их между фоном и передним планом. Ctrl+Z приостанавливает задание на переднем плане (посылает SIGTSTP). wait блокирует выполнение до завершения фоновых задач — необходимо для скриптов, запускающих параллельную работу. disown удаляет задание из таблицы заданий оболочки, поэтому оно продолжает работать после выхода (в отличие от nohup, работает с уже запущенными заданиями).

bash
# run in background (append &)
sleep 100 &
# [1] 12345   (job 1, PID 12345)

# list jobs
jobs
jobs -l            # with PIDs

# bring to foreground / send to background
fg                 # foreground the current job
fg %1              # foreground job 1
bg %2              # background job 2 (continue running)

# suspend a running foreground job: Ctrl+Z
# then resume in background:
bg

# wait for background jobs
sleep 5 &
wait                # wait for all background jobs
wait $!             # wait for the last background job
wait 12345          # wait for specific PID

# disown: detach from shell (survives logout)
long_task &
disown %1

kill и сигналы

kill посылает сигналы процессам. SIGTERM (по умолчанию) просит процесс корректно завершиться — он может очистить ресурсы. SIGKILL (-9) принудительный и немедленный; процесс не может его перехватить или игнорировать, поэтому может оставить ресурсы в плохом состоянии. Всегда сначала попробуйте SIGTERM, подождите, затем SIGKILL только при необходимости. killall/pkill завершают по имени. pkill -f сопоставляет полную командную строку (гибче). Используйте kill -l для вывода всех имён сигналов.

bash
# send signals to processes
kill 12345              # SIGTERM (graceful, default)
kill -15 12345          # SIGTERM explicitly
kill -9 12345           # SIGKILL (force, cannot be caught)
kill -HUP 12345         # SIGHUP (reload config)

# kill by name
killall nginx           # kill all nginx processes
pkill -f "python app.py"  # match full command line

# common signals
# SIGTERM (15) - graceful termination (default)
# SIGKILL (9)  - force kill (cannot be caught/ignored)
# SIGINT (2)   - interrupt (Ctrl+C)
# SIGHUP (1)   - hangup (often reload config)
# SIGSTOP (19) - pause (cannot be caught)
# SIGCONT (18) - resume
# SIGTSTP (20) - terminal stop (Ctrl+Z)

# list all signals
kill -l

# priority: try SIGTERM first, SIGKILL as last resort
kill 12345; sleep 2; kill -9 12345 2>/dev/null

trap — обработка сигналов

trap регистрирует команды, выполняемые при получении скриптом сигнала — необходимо для очистки. Псевдосигнал EXIT срабатывает при любом выходе (нормальном, по ошибке или прерывании), что делает его идеальным для удаления временных файлов. Всегда устанавливайте traps до создания ресурсов, которые они очищают. Распространённый шаблон: trap cleanup EXIT INT TERM. trap '' SIGNAL игнорирует сигнал; trap - SIGNAL восстанавливает поведение по умолчанию. Так надёжные скрипты гарантируют очистку даже при прерывании.

bash
# clean up on exit (EXIT signal)
trap 'rm -f $TMPFILE' EXIT
TMPFILE=$(mktemp)
echo "working with $TMPFILE"
# when script exits (normally or via error), TMPFILE is removed

# handle Ctrl+C (SIGINT)
trap 'echo "Caught Ctrl+C, exiting"; exit 130' INT

# handle multiple signals
cleanup() {
    echo "Cleaning up..."
    rm -rf "$WORKDIR"
    exit
}
trap cleanup INT TERM EXIT

# ignore a signal
trap '' INT              # ignore Ctrl+C
trap - INT               # restore default behavior

# reload on SIGHUP
trap 'echo "reloading config"; source config.sh' HUP

# list current traps
trap -p

Инспекция процессов

ps показывает снимок процессов; top/htop — в реальном времени. pstree отображает иерархию родитель-потомок. pgrep находит PID по имени (безопаснее, чем ps|grep). lsof -i :PORT находит, какой процесс использует порт — необходимо для отладки ошибок 'порт занят'. ss (статистика сокетов) — современная замена netstat. Шаблон ps aux | grep повсеместно распространён, но pgrep чище. Используйте --sort для поиска главных потребителей ресурсов.

bash
# list processes
ps aux                   # all processes (BSD style)
ps -ef                   # all processes (System V style)
ps -ef | grep nginx      # find nginx processes

# interactive viewer (like Task Manager)
top                      # real-time, all processes
htop                     # nicer interactive viewer (install separately)

# process tree
pstree                   # tree view
pstree -p                # with PIDs

# find process by name/port
pgrep -f "node app"      # print PIDs matching
pgrep -fl "node"         # full command line

# who's listening on a port
lsof -i :8080            # process using port 8080
ss -tlnp                 # all listening TCP ports
netstat -tlnp            # legacy alternative

# process resource usage
top -b -n 1 | head       # snapshot
ps aux --sort=-%mem | head  # top memory users

nohup, disown и tmux

nohup заставляет процесс игнорировать SIGHUP, чтобы он пережил выход из системы — вывод направляется в nohup.out. disown делает то же самое для уже запущенного фонового задания. setsid запускает процесс в новой сессии, полностью отсоединяя его. Для длительной интерактивной работы лучше tmux или screen: они сохраняют полную терминальную сессию, к которой можно переподключиться позже, поэтому даже текстовые редакторы переживают отключение. Так сисадмины управляют удалёнными серверами.

bash
# nohup: run immune to hangups (survives logout)
nohup ./long_script.sh &
# output goes to nohup.out by default
nohup ./server.sh > server.log 2>&1 &

# disown: detach an already-running job
./long_task &
disown %1

# setsid: start in a new session
setsid ./daemon.sh &

# tmux: terminal multiplexer (persistent sessions)
tmux                    # start new session
tmux new -s work        # named session
tmux ls                 # list sessions
tmux attach -t work     # reattach
# inside tmux: Ctrl+B then D to detach
# processes keep running after detach

# screen: alternative to tmux
screen -S work
screen -ls
screen -r work

# check if a process is running
pgrep -x nginx && echo "running" || echo "stopped"
09

Скриптинг и продвинутые темы

Shebang и структура скрипта

Shebang (#!) сообщает ядру, какой интерпретатор использовать. #!/usr/bin/env bash наиболее переносим. Хорошо структурированный скрипт начинается с 'set -euo pipefail' для безопасности, определяет usage() и разбирает аргументы через getopts (для коротких флагов) или ручной цикл (для длинных флагов). OPTIND отслеживает следующий аргумент; сдвигайте после разобранных опций, чтобы $1 был первым позиционным аргументом. Такая структура делает скрипты надёжными и удобными для пользователя.

bash
#!/usr/bin/env bash
# ^ portable shebang (finds bash via PATH)
# #!/bin/bash          # absolute path (common)
# #!/usr/bin/bash       # on some systems
# #!/bin/sh             # POSIX sh (most portable)

set -euo pipefail        # strict mode (see below)

# script metadata
readonly PROGNAME=$(basename "$0")
readonly VERSION="1.0.0"

usage() {
    cat << EOF
Usage: $PROGNAME [OPTIONS] FILE
Options:
  -h, --help     Show this help
  -v, --version  Show version
  -o OUTPUT      Output file
EOF
}

# parse options with getopts
while getopts "hvo:" opt; do
    case $opt in
        h) usage; exit 0 ;;
        v) echo $VERSION; exit 0 ;;
        o) output=$OPTARG ;;
        *) usage; exit 1 ;;
    esac
done
shift $((OPTIND - 1))

Строгий режим (set -euo pipefail)

set -e немедленно завершает работу при сбое любой команды (ненулевой возврат) — ловит ошибки рано. set -u считает ссылки на неустановленные переменные ошибками. pipefail делает конвейер возвращающим ненулевое значение, если ЛЮБАЯ команда в нём завершилась неудачей (по умолчанию учитывается только статус последней команды). Вместе они отлавливают большинство багов. Используйте 'cmd || true' для допустимых ожидаемых неудач и 'if ! cmd' для явных проверок. Некоторые команды (grep, test) правомерно возвращают ненулевое значение, поэтому оборачивайте их.

bash
# the 'strict mode' trio
set -e             # exit on error (any non-zero status)
set -u             # error on unset variable
set -o pipefail    # pipeline fails if any command fails
# combined: set -euo pipefail

# example: without -e, errors are silently ignored
mkdir /root/nope   # fails (permission denied)
echo "still running"  # this runs anyway!

# with set -e, the script exits at the failure
set -e
mkdir /root/nope   # script exits here
echo "never reached"

# handle expected failures
set -e
if ! grep -q "x" file; then
    echo "not found"   # grep returning 1 won't kill script
fi

# temporarily disable -e
set +e
risky_command
set -e

# || true to ignore a command's failure
mkdir -p dir || true

Методы отладки

set -x (xtrace) выводит каждую команду перед выполнением — инструмент отладки №1. PS4 настраивает приглашение (добавление номеров строк помогает локализовать проблемы). bash -n проверяет синтаксис без выполнения. trap ERR запускает команду при любой ошибке — идеально для логирования места сбоя. Для сложных скриптов оборачивайте подозрительные функции set -x/+x для трассировки именно этой части. Комбинируйте с set -e для остановки на первой ошибке и инспекции.

bash
# trace execution: print each command before running
set -x              # turn on
set +x              # turn off
bash -x script.sh   # run with tracing from the start

# verbose: print input lines as read
set -v

# combine: -xv for full detail
bash -xv script.sh

# print commands with line numbers
export PS4='+ ${BASH_SOURCE}:${LINENO}: '
set -x

# check syntax without executing
bash -n script.sh   # syntax check only

# step through interactively
bash -x script.sh 2>&1 | less

# debug a specific function
debug_func() {
    set -x
    actual_func "$@"
    set +x
}

# use trap ERR to catch errors
trap 'echo "Error on line $LINENO" >&2' ERR

Арифметика и математика

Bash выполняет только целочисленную арифметику с $(( )). Для чисел с плавающей точкой передавайте выражения в bc (scale=N устанавливает количество знаков после запятой) или используйте awk. bc -l загружает математическую библиотеку (sqrt, sin, cos и т. д.). $RANDOM даёт псевдослучайное целое 0-32767; для криптографической случайности используйте /dev/urandom. Команда (( )) поддерживает +=, -=, *=, /=, %= и ++/-- как C. Для серьёзной математики Python или awk — лучший выбор.

bash
# integer arithmetic
echo $((5 + 3))         # 8
echo $((10 / 3))        # 3 (integer division)
echo $((10 % 3))        # 1 (modulo)
echo $((2 ** 8))        # 256 (exponent)

# floating point with bc
echo "scale=2; 10/3" | bc       # 3.33
echo "3.14 * 2" | bc            # 6.28
result=$(echo "scale=4; $a / $b" | bc)

# floating point with awk
awk 'BEGIN {printf "%.2f\n", 10/3}'   # 3.33
awk "BEGIN {print $a * $b}"

# math functions with bc -l
echo "scale=4; s(0)" | bc -l    # 0 (sine, radians)
echo "scale=4; sqrt(16)" | bc -l # 4.0000

# random numbers
echo $RANDOM              # 0-32767
echo $((RANDOM % 100))    # 0-99
head -c 4 /dev/urandom | od -An -tu4   # 32-bit random

# increment/decrement
n=5
((n++)); echo $n          # 6
((n+=10)); echo $n        # 16

Regex и сопоставление шаблонов

В Bash три системы шаблонов: glob (*.txt, ?, [abc]) для имён файлов, расширенные glob (extglob: !(), @(), *()) для более сложного сопоставления и ERE regex (=~ в [[ ]]). =~ захватывает группы в BASH_REMATCH (индекс 0 = полное совпадение, 1+ = группы). Regex использует синтаксис ERE (как grep -E). Включите extglob для мощного отрицания и чередования в шаблонах имён файлов. Regex — одна из самых полезных возможностей Bash для валидации ввода.

bash
# glob patterns (filename matching)
*.txt          # any .txt file
file?.txt      # single char: file1.txt, fileA.txt
file[0-9].txt  # digit: file0.txt ... file9.txt
file[!0-9].txt # NOT a digit

# extended globs (shopt -s extglob)
shopt -s extglob
echo !(*.bak)       # everything except .bak files
echo @(*.jpg|*.png) # jpg or png
echo *(foo)         # zero or more "foo"

# regex with =~ in [[ ]]
if [[ $email =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
    echo "valid email"
fi

# capture groups with BASH_REMATCH
if [[ $date =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})$ ]]; then
    echo "Year: ${BASH_REMATCH[1]}"
    echo "Month: ${BASH_REMATCH[2]}"
    echo "Day: ${BASH_REMATCH[3]}"
fi

# case-insensitive regex
shopt -s nocasematch
if [[ $s =~ ^hello ]]; then echo "starts with hello"; fi
10

Углублённое изучение grep

Базовые шаблоны grep

grep ищет текст по шаблонам. -i игнорирует регистр; -w сопоставляет целые слова (предотвращает совпадение 'errors' при поиске 'error'); -v инвертирует (несовпадающие строки); -c считает; -n показывает номера строк; -l выводит только имена файлов с совпадениями; -h подавляет префикс имени файла при поиске по нескольким файлам. По умолчанию grep использует Basic Regular Expressions (BRE), где метасимволы требуют экранирования обратной косой чертой. Используйте -E для Extended Regex (чище синтаксис) или -F для фиксированных строк (без regex, быстрее для литерального поиска).

bash
# Basic search (literal string)
grep "error" logfile.txt

# Case-insensitive
grep -i "error" logfile.txt

# Whole word match
grep -w "error" logfile.txt

# Invert match (lines NOT matching)
grep -v "debug" logfile.txt

# Count matches
grep -c "error" logfile.txt

# Show line numbers
grep -n "error" logfile.txt

# Multiple files with filename prefix
grep "error" *.log

# Suppress filename prefix (single file behavior)
grep -h "error" *.log

# Only show filenames with matches
grep -l "error" *.log

grep с regex

grep -E (или egrep) использует Extended Regular Expressions с более чистым синтаксисом: +, ?, |, () работают без экранирования. ^ и $ привязывают к началу/концу строки. [] определяет классы символов; {} задаёт повторение. -P включает Perl-совместимый regex (PCRE) с возможностями вроде \d, \w, lookaheads — но это специфично для GNU и непереносимо. Для сложного regex рассмотрите ripgrep (rg), который быстрее и использует PCRE-подобный синтаксис по умолчанию. Всегда заключайте шаблоны в кавычки, чтобы предотвратить интерпретацию оболочкой специальных символов вроде $ и *.

bash
# Extended regex (-E or egrep)
grep -E "error|warning|fatal" logfile.txt

# Anchors: ^ start, $ end
grep -E "^ERROR:" logfile.txt      # lines starting with ERROR:
grep -E "completed$" logfile.txt   # lines ending with completed

# Character classes
grep -E "[0-9]{4}-[0-9]{2}-[0-9]{2}" dates.txt  # date format
grep -E "[A-Z][a-z]+" names.txt                  # Capitalized words

# Quantifiers
grep -E "ab+c" file.txt    # one or more b's
grep -E "ab*c" file.txt    # zero or more b's
grep -E "ab?c" file.txt    # zero or one b

# Alternation and grouping
grep -E "(cat|dog) food" file.txt

# PCRE (-P, GNU grep only)
grep -P "\d{3}-\d{4}" phones.txt  # \d for digits

Контекст и управление выводом grep

Флаги контекста (-B, -A, -C) показывают окружающие строки, необходимые для понимания записей логов. -o выводит только совпавшую часть (полезно для извлечения URL, чисел и т. д.). --color подсвечивает совпадения в терминале. -r выполняет рекурсивный поиск (исключает символьные ссылки); -R следует по символьным ссылкам. --include/--exclude/--exclude-dir фильтруют, какие файлы искать — незаменимо для больших кодовых баз (всегда исключайте node_modules, .git, vendor). -a принудительно трактует бинарные файлы как текст. Для поиска по коду ripgrep (rg) — современная, более быстрая альтернатива с разумными значениями по умолчанию.

bash
# Show context: lines before/after match
grep -B 2 "error" log.txt    # 2 lines Before
grep -A 3 "error" log.txt    # 3 lines After
grep -C 2 "error" log.txt    # 2 lines Context (both)

# Only show matched part (not full line)
grep -o "https?://[^ ]+" urls.txt

# Color highlight matches
grep --color=auto "error" log.txt

# Recursive search
grep -r "TODO" ./src/
grep -R "TODO" ./src/        # follows symlinks

# Include/exclude file patterns
grep -r --include="*.py" "import" .
grep -r --exclude="*.test.js" "TODO" .
grep -r --exclude-dir=node_modules "TODO" .

# Binary files
grep -a "pattern" binary_file  # treat as text

grep со stdin и конвейерами

grep наиболее мощен в конвейерах. Трюк [n]ginx предотвращает совпадение grep с собственным процессом: скобки делают шаблон несовпадающим с литеральной строкой 'grep' в списке процессов. zgrep ищет по сжатым файлам без ручной распаковки. pgrep — специализированный поиск процессов (лучше, чем ps | grep). grep -rl выводит файлы, содержащие шаблон; передача по конвейеру в xargs grep ищет внутри этих файлов другой шаблон — распространённая техника кодовой археологии. Для интерактивного поиска по коду используйте ripgrep или ack, созданные для исходного кода.

bash
# Search command output
ps aux | grep nginx
ps aux | grep "[n]ginx"  # trick: prevents matching grep itself

# Search compressed logs
zcat log.gz | grep "error"
zgrep "error" log.gz     # direct

# Chain multiple greps
cat log.txt | grep "error" | grep -v "timeout" | grep -c

# Extract and filter
grep -oE "[0-9.]+" response.txt | sort -n | uniq

# Find processes excluding grep
pgrep -f "node server.js"  # better than ps | grep

# Search command history
history | grep "git rebase"

# Find files containing pattern, then search more
grep -rl "config" . | xargs grep "database"

Статус завершения grep и скриптинг

Статус завершения grep делает его идеальным для скриптинга: 0 (совпадение найдено), 1 (нет совпадения), 2 (ошибка). -q (quiet) подавляет вывод для чисто условных проверок. В скриптах с set -e возврат grep 1 (нет совпадения) завершил бы скрипт — используйте '|| true' для предотвращения. Шаблон while-read обрабатывает совпавшие строки по одной. grep -c возвращает количество (0, если нет совпадений), которое можно сравнивать численно. Эта скриптовая возможность делает grep строительным блоком для мониторинга логов, скриптов валидации и проверок CI/CD.

bash
# grep returns exit codes for scripting
# 0 = match found, 1 = no match, 2 = error

if grep -q "error" /var/log/syslog; then
    echo "Errors found!"
    # send alert, etc.
fi

# Silent check (-q), no output
grep -q "^$" file.txt && echo "has blank lines"

# Use in while loop
grep "pattern" file.txt | while read -r line; do
    echo "Found: $line"
done

# Count and branch
errors=$(grep -c "ERROR" log.txt)
if [ "$errors" -gt 10 ]; then
    echo "Too many errors: $errors"
fi

# grep with set -e (exits on non-zero)
set -e
grep "missing" file.txt || true  # prevent exit
11

Углублённое изучение sed

Подстановка в sed

sed (потоковый редактор) преобразует текст построчно. Команда s подставляет текст: s/pattern/replacement/flags. g (глобально) заменяет все вхождения в строке; без него заменяется только первое совпадение. Можно ограничить операции конкретными строками (по номеру, диапазону или шаблону). -i редактирует файлы по месту (опасно — всегда сначала тестируйте без -i или используйте -i.bak для резервной копии). sed обрабатывает каждую строку независимо. Разделитель не обязан быть / — используйте s|old|new|g, когда шаблоны содержат слэши (например, пути файлов).

bash
# Basic substitution (first occurrence per line)
sed 's/old/new/' file.txt

# Replace all occurrences
sed 's/old/new/g' file.txt

# Case-insensitive (GNU sed)
sed 's/old/new/gi' file.txt

# Replace Nth occurrence
sed 's/old/new/2' file.txt  # 2nd occurrence only

# Only on lines matching pattern
sed '/error/s/old/new/' file.txt

# Only on specific line numbers
sed '5s/old/new/' file.txt      # line 5 only
sed '5,10s/old/new/g' file.txt  # lines 5-10
sed '$s/old/new/' file.txt      # last line

# In-place edit (modifies file)
sed -i 's/old/new/g' file.txt
sed -i.bak 's/old/new/g' file.txt  # keeps backup

sed с regex и группами захвата

sed -E (или -r) использует Extended Regular Expressions с более чистым синтаксисом (без обратной косой черты перед скобками/фигурными скобками). Группы захвата в замене ссылаются как \1, \2 и т. д. & представляет полное совпадение. Пример переформатирования даты показывает мощь: отдельный захват года, месяца, дня и перестановка. Несколько команд можно объединить точкой с запятой (s/.../.../;s/.../.../). Всегда экранируйте специальные символы в шаблонах (., *, [ и т. д.) и экранируйте / в заменах (или используйте другой разделитель).

bash
# Extended regex (-E or -r)
sed -E 's/[0-9]+/NUMBER/g' file.txt

# Capture groups with \1, \2 (BRE)
sed 's/\(foo\)\(bar\)/\2\1/' file.txt  # swap: foobar -> barfoo

# Capture groups with () (ERE, cleaner)
sed -E 's/(foo)(bar)/\2\1/' file.txt

# Reformat date: 2025-01-15 -> 15/01/2025
sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/' file.txt

# Add prefix to lines
sed 's/^/>> /' file.txt

# Add suffix
sed 's/$/ <<</' file.txt

# Remove leading/trailing whitespace
sed 's/^[ \t]*//;s/[ \t]*$//' file.txt

# Use & for full match
sed 's/[0-9]\{3\}/[&]/g' file.txt  # wrap 3 digits in brackets

Удаление и печать в sed

Команда d удаляет строки; p печатает строки. С -n (без автопечати) только явные команды p дают вывод — это превращает sed в избирательный принтер (как grep). Удаление пустых строк (sed '/^$/d') — частая очистка. sed -n '5,10p' эквивалентно sed -n '5,10p' или комбинациям head/tail. Синтаксис ~ (0~3p) печатает каждую 3-ю строку (расширение GNU). Помните: без -n sed печатает каждую строку (возможно, изменённую); с -n он ничего не печатает, если не использовать p. Эта двойственность делает sed одновременно редактором и фильтром.

bash
# Delete lines
sed '5d' file.txt              # delete line 5
sed '5,10d' file.txt           # delete lines 5-10
sed '/pattern/d' file.txt      # delete matching lines
sed '/^$/d' file.txt           # delete blank lines
sed '/^[ \t]*$/d' file.txt    # delete whitespace-only lines
sed '$d' file.txt              # delete last line

# Print only specific lines (like head/tail)
sed -n '5p' file.txt           # print line 5 only
sed -n '5,10p' file.txt        # print lines 5-10
sed -n '/pattern/p' file.txt   # print matching lines (like grep)
sed -n '5,${/pattern/p}' file.txt  # from line 5 to end

# Print with line numbers
sed '=' file.txt | sed 'N;s/\n/\t/'

# Print every Nth line
sed -n '0~3p' file.txt  # every 3rd line (GNU)

Многострочность и hold space в sed

Hold space в sed позволяет многострочные операции — pattern space (текущая строка) можно сохранить в hold space и получить позже. h/H копирует/добавляет в hold; g/G копирует/добавляет из hold; x обменивает их. Трюк объединения строк (:a;N;$!ba;s/\n/ /g) считывает весь файл в pattern space, затем заменяет переводы строк. N добавляет следующую строку в pattern space. Эти расширенные возможности делают sed полным по Тьюрингу, но также очень криптическим. Для сложных многострочных преобразований awk или Perl более читаемы. Используйте sed для простых построчных правок; awk — для логики по полям или многострочной.

bash
# Join lines (replace newline with space)
sed ':a;N;$!ba;s/\n/ /g' file.txt

# Join every 2 lines
sed 'N;s/\n/ /' file.txt

# Print paragraph (blocks separated by blank lines)
sed '/./{H;d;};x;s/\n/ /g' file.txt

# Reverse line order (tac alternative)
sed '1!G;h;$!d' file.txt

# Double-space a file
sed 'G' file.txt

# Remove last line of each paragraph
sed -n '/^$/{p;h;};/./{H;};/^$/{x;s/\n.*//;p;}' file.txt

# Hold space (h,H,g,G,x) stores lines between cycles
# h: copy pattern space to hold
# H: append pattern space to hold
# g: copy hold to pattern space
# G: append hold to pattern space
# x: exchange pattern and hold

sed в конвейерах и скриптах

sed преуспевает в конвейерах для преобразования текста. При подстановке переменных экранируйте специальные символы (& и \), чтобы предотвратить интерпретацию. Несколько флагов -e применяют несколько команд последовательно. -f считывает команды из файла (полезно для сложных скриптов). Преобразование CSV в TSV показывает практическое применение. Для редактирования файлов конфигурации в скриптах всегда проверяйте изменение (grep после sed) и рассмотрите использование специализированных инструментов (jq для JSON, yq для YAML). Сила sed — в простых, быстрых, построчных правках — это основа Unix-обработки текста наряду с grep и awk.

bash
# Clean up output in pipeline
cat messy.txt | sed 's/[ \t]*$//' | sed '/^$/d' | sort

# Extract and transform
echo "name=John;age=30" | sed 's/;/\n/g' | sed 's/=/: /'

# Modify config files in scripts
sed -i "s/PORT=8080/PORT=$NEW_PORT/" config.env

# Escape special chars for safe substitution
ESCAPED=$(printf '%s\n' "$URL" | sed 's/[&\]/\\&/g')
sed "s|URL|$ESCAPED|g" template.txt

# Multiple substitutions
sed -e 's/foo/bar/g' -e 's/baz/qux/g' file.txt

# Read commands from file
sed -f script.sed input.txt

# Convert CSV to TSV
sed 's/,/\t/g' data.csv
12

Углублённое изучение awk

Основы awk и поля

awk автоматически разбивает каждую строку на поля ($1, $2, ..., $NF). -F устанавливает входной разделитель полей; OFS — выходной. $0 — вся строка. NR (Number of Records) — номер строки; NF (Number of Fields) — количество полей в текущей строке. awk обрабатывает каждую строку через пары шаблон-действие: pattern { action }. Без шаблона действие выполняется для каждой строки. Без действия строка печатается. Это делает awk идеальным для извлечения колоночных данных из CSV, TSV, /etc/passwd и логов.

bash
# Print specific fields (default delimiter: whitespace)
awk '{print $1}' file.txt        # first field
awk '{print $1, $3}' file.txt    # first and third
awk '{print $NF}' file.txt       # last field
awk '{print $(NF-1)}' file.txt   # second-to-last

# Custom field separator
awk -F: '{print $1, $7}' /etc/passwd   # colon-separated
awk -F',' '{print $2}' data.csv        # CSV
awk -F'\t' '{print $1}' data.tsv      # TSV

# Print entire line ($0)
awk '{print}' file.txt
awk '{print NR, $0}' file.txt  # with line number

# NR = record (line) number
# NF = number of fields in current record
# NR and NF are built-in variables

# Change output separator
awk 'BEGIN{OFS="|"} {print $1, $2, $3}' file.txt

Шаблоны и условия в awk

Шаблоны awk могут быть regex (/pattern/), сравнениями ($3 > 100), номерами строк (NR == 5) или диапазонами (/start/,/end/). ~ и !~ применяют regex к конкретным полям. Условия можно комбинировать с &&, || и !. Это делает awk мощным фильтром — более выразительным, чем grep, поскольку можно численно или строково сравнивать значения полей. Шаблон диапазона (/start/,/end/) печатает все строки между двумя маркерами (включительно). awk вычисляет условия построчно; если истина, выполняется действие. Без действия по умолчанию {print}.

bash
# Pattern: only process matching lines
awk '/error/ {print}' log.txt          # like grep
awk '$3 > 100' data.txt                # 3rd field > 100
awk 'NR == 5' file.txt                 # line 5 only
awk 'NR >= 10 && NR <= 20' file.txt    # lines 10-20
awk 'NF == 0' file.txt                 # empty lines
awk '$1 == "ERROR"' log.txt            # first field equals ERROR

# Range pattern: from start to end
awk '/start/,/end/' file.txt

# Combine conditions
awk '$3 > 50 && $3 < 100 {print $1, $3}' data.txt
awk '$1 == "GET" || $1 == "POST" {print}' access.log

# Negation
awk '!/debug/ {print}' log.txt  # lines NOT containing debug

# Field comparison with regex
awk '$2 ~ /^[0-9]+$/' file.txt   # 2nd field is numeric
awk '$2 !~ /pattern/' file.txt   # 2nd field doesn't match

BEGIN/END и переменные в awk

BEGIN выполняется до чтения любого ввода (идеально для инициализации, заголовков, установки переменных). END выполняется после обработки всего ввода (идеально для итогов, сумм). Пользовательские переменные не требуют объявления — по умолчанию 0 (число) или пусто (строка). -v передаёт внешние переменные в awk. Это делает awk мини-языком программирования для обработки данных: sum, average, min, max, count — всё за один проход. Шаблон 'NR == 1 {max = $1}' инициализирует max из первой строки, затем последующие строки обновляют его. Это намного эффективнее нескольких конвейеров grep/sort/cut.

bash
# BEGIN: runs before processing (setup)
# END: runs after processing (summary)
awk 'BEGIN {print "Total:"} {sum += $1} END {print sum}' nums.txt

# Count lines
awk 'END {print NR " lines"}' file.txt

# Sum a column
awk '{sum += $2} END {print "Total:", sum}' sales.csv

# Average
awk '{sum += $2; count++} END {print sum/count}' data.txt

# Find max value
awk 'NR == 1 {max = $1} $1 > max {max = $1} END {print max}' nums.txt

# User-defined variables (no declaration needed)
awk '{total += $1; if ($1 > max) max = $1}
     END {print "Sum:", total, "Max:", max, "Avg:", total/NR}'

# Pass variables with -v
awk -v threshold=50 '$1 > threshold {print}' data.txt
awk -v date="$(date +%Y-%m-%d)" '{print date, $0}' file.txt

Поток управления в awk

awk имеет полный поток управления: if/else, for, while и ассоциативные массивы (ключ-значение). Это делает его полноценным языком обработки данных. Пример подсчёта слов (words[$1]++) — классический случай применения awk — подсчёт вхождений каждого значения в столбце с последующей сортировкой по частоте. Массивы в awk ассоциативные (как словари), индексируются строкой или числом. for (key in array) перебирает ключи (неупорядоченно — передавайте через sort). awk может заменить целые конвейеры cut, sort, uniq и wc одним эффективным проходом. Для сложной обработки данных awk часто яснее, чем цепочки Unix-команд.

bash
# if-else
awk '{
  if ($3 >= 90) grade = "A"
  else if ($3 >= 80) grade = "B"
  else if ($3 >= 70) grade = "C"
  else grade = "F"
  print $1, grade
}' students.txt

# for loop
awk 'BEGIN {
  for (i = 1; i <= 5; i++) print i, i*i
}'

# for-in (iterate array)
awk '{count[$1]++} END {
  for (key in count) print key, count[key]
}' access.log | sort -rnk2

# while loop
awk '{
  i = 1
  while (i <= NF) {
    print i, $i
    i++
  }
}' file.txt

# Arrays (associative)
awk '{words[$1]++} END {
  for (w in words) print words[w], w
}' file.txt | sort -rn | head

Практические примеры awk

Эти практические примеры показывают реальную мощь awk. Подсчёт частоты IP необходим для анализа логов. Сводка размеров файлов по расширению использует split() для извлечения расширений. Фильтрация CSV по значению столбца заменяет сложные конвейеры grep/sed. Вычисление перцентилей демонстрирует математические возможности awk (asorti сортирует индексы массива). Переформатирование столбцов (изменение разделителя и выбор полей) — частая задача ETL. awk — инструмент по умолчанию для обработки структурированного текста — когда данные имеют столбцы/поля, awk почти всегда правильный выбор. Для JSON используйте jq; для CSV — awk или csvkit.

bash
# Top 10 IP addresses from access log
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
# Or entirely in awk:
awk '{ip[$1]++} END {for (i in ip) print ip[i], i}' access.log | sort -rn | head

# Sum file sizes by extension
ls -l | awk '{split($NF, a, "."); ext=a[2]; size[ext]+=$5}
  END {for (e in size) print e, size[e]}' | sort -rnk2

# Filter CSV by column value
awk -F',' '$3 == "active" {print $1, $2}' users.csv

# Calculate response time percentiles from log
awk '{times[NR] = $2} END {
  n = asorti(times, sorted)
  print "p50:", sorted[int(n*0.5)]
  print "p95:", sorted[int(n*0.95)]
  print "p99:", sorted[int(n*0.99)]
}' response_times.txt

# Reformat columns
awk -F',' 'BEGIN{OFS="|"} {print $1, $2, $4}' data.csv
13

find и xargs

find по имени и типу

find ищет по файловой системе по различным критериям. -name сопоставляет имя файла (с учётом регистра); -iname без учёта регистра. -type f/d/l фильтрует по типу файла. -path сопоставляет полный путь; -regex использует regex по полному пути. -o (или) объединяет условия; используйте \( \) для группировки. -maxdepth ограничивает глубину рекурсии (важно для производительности на больших файловых системах). find выводит пути; комбинируйте с -exec или xargs для действий над результатами. Всегда заключайте шаблоны в кавычки, чтобы предотвратить glob-раскрытие оболочки. Для поиска по коду ripgrep (rg) быстрее, но find гибче для файловых операций.

bash
# Find by name (exact)
find . -name "config.txt"

# Case-insensitive name
find . -iname "config.txt"

# Wildcard pattern
find . -name "*.py"
find . -name "test_*"

# Find by type
find . -type f -name "*.js"   # files only
find . -type d -name "node_modules"  # directories
find . -type l                # symlinks

# Find by path (full path match)
find . -path "*/src/*.test.js"

# Find by regex
find . -regex ".*\.\(py\|js\)$"

# Multiple name patterns
find . \( -name "*.py" -o -name "*.js" \)

# Max depth (don't recurse too deep)
find . -maxdepth 2 -name "*.txt"

find по времени и размеру

Поиск find по времени необходим для очистки и аудита. -mtime (изменение), -atime (доступ), -ctime (изменение метаданных) используют дни; -mmin/-amin/-cmin используют минуты. - (меньше) и + (больше) предваряют значение. Размер использует суффиксы: c (байты), k (КБ), M (МБ), G (ГБ). -empty находит файлы нулевой длины или пустые каталоги. -perm проверяет права: точное совпадение (644), все биты установлены (-u+x) или любой бит установлен (/4000). Поиск SUID-файлов (/4000) — техника аудита безопасности. Эти критерии можно комбинировать с -a (и, по умолчанию) и -o (или).

bash
# Find by modification time
find . -mtime -1    # modified in last 24 hours
find . -mtime +7    # modified more than 7 days ago
find . -mtime 7     # modified exactly 7 days ago

# Access time (-atime) and change time (-ctime)
find . -atime -1    # accessed in last 24h
find . -ctime -1    # status changed in last 24h

# Minutes instead of days
find . -mmin -30    # modified in last 30 minutes

# Find by size
find . -size +100M   # larger than 100MB
find . -size -1k     # smaller than 1KB
find . -size 10M     # exactly 10MB

# Find empty files/directories
find . -empty -type f
find . -empty -type d

# Find by permissions
find . -perm 644     # exactly 644
find . -perm -u+x    # has execute for user
find / -perm /4000   # SUID files (security audit)

find -exec и -delete

-exec запускает команду для каждого результата. {} — заполнитель имени файла; \; завершает команду (запускается для каждого файла); + объединяет файлы (запускается один раз со всеми файлами — эффективнее). -delete удаляет совпавшие файлы (быстрее, чем -exec rm, но сначала протестируйте с -print). Шаблон chmod (каталоги 755, файлы 644) — частая настройка веб-сервера. -exec с + предпочтительнее для команд, принимающих несколько файлов (grep, ls, wc). Для удаления всегда запускайте с -print сначала для проверки, что будет удалено, затем замените на -delete. -ok вместо -exec запрашивает подтверждение для каждого файла.

bash
# Execute command on each result
find . -name "*.log" -exec ls -lh {} \;

# Execute with + (batch, more efficient)
find . -name "*.log" -exec ls -lh {} +

# Delete files (DANGEROUS - test first!)
find /tmp -name "*.tmp" -type f -delete
# Safer: use -exec rm
find /tmp -name "*.tmp" -type f -exec rm -i {} \;

# Execute a complex command
find . -name "*.py" -exec wc -l {} \; | sort -rn | head

# Change permissions
find . -type d -exec chmod 755 {} \;  # dirs to 755
find . -type f -exec chmod 644 {} \;  # files to 644

# Find and grep
find . -name "*.py" -exec grep "TODO" {} +

# Print before deleting (confirmation)
find . -name "*.bak" -print -delete

Основы xargs

xargs преобразует stdin в аргументы команды. Это мост между выводом find и командами, не читающими stdin. -n ограничивает количество аргументов на команду; -I {} определяет заполнитель для кастомного позиционирования. -0 (с find -print0) корректно обрабатывает имена файлов с пробелами/переводами строк — всегда используйте эту пару для безопасности. -P включает параллельное выполнение (отлично для CPU-интенсивных задач вроде конвертации изображений). -t (trace) показывает команды перед выполнением; -p запрашивает подтверждение. xargs необходим, когда -exec слишком медленный (по одному процессу на файл) — xargs эффективно батчит аргументы.

bash
# Basic: take stdin and pass as arguments
echo "file1 file2 file3" | xargs rm

# One argument per line
find . -name "*.bak" | xargs rm

# Control arguments per command (-n)
echo "1 2 3 4 5" | xargs -n 2 echo  # echo 1 2; echo 3 4; echo 5

# Show commands before running (-t, verbose)
find . -name "*.log" | xargs -t rm

# Prompt before each command (-p)
find . -name "*.tmp" | xargs -p rm

# Handle filenames with spaces (-0 with find -print0)
find . -name "*.txt" -print0 | xargs -0 grep "pattern"

# Replace string (-I)
find . -name "*.py" | xargs -I {} cp {} /backup/

# Parallel execution (-P)
find . -name "*.png" | xargs -P 4 -I {} convert {} {}.thumb.png

# Limit with -L (lines per command)
cat urls.txt | xargs -L 1 curl -O

Шаблоны find + xargs

find + xargs — классический Unix-шаблон для пакетных файловых операций. -print0 | xargs -0 — безопасная комбинация для имён файлов с пробелами или специальными символами. Шаблон массового grep намного быстрее, чем -exec grep (один процесс grep против множества). Параллельный xargs (-P N) резко ускоряет CPU-интенсивные задачи вроде аудио/видео конвертации. Шаблон архивации (find старых логов, tar их) — распространённая техника ротации логов. Всегда используйте -print0/-0 для надёжности — без этого имена файлов с пробелами, кавычками или переводами строк сломают конвейер. Эта комбинация фундаментальна для системного администрирования Unix.

bash
# Find and bulk grep (efficient)
find . -name "*.py" -print0 | xargs -0 grep -l "import django"

# Find large files and show sizes
find . -type f -size +100M -print0 | xargs -0 ls -lhS

# Find and archive
find . -name "*.log" -mtime +30 -print0 |
  xargs -0 tar -czf old_logs.tar.gz

# Find and batch rename
find . -name "*.JPG" | xargs -I {} mv {} {}.bak
# Or with rename command:
find . -name "*.JPG" -exec rename 's/\.JPG$/.jpg/' {} +

# Find empty directories and remove
find . -type d -empty -print0 | xargs -0 rmdir

# Find recently modified files and copy
find . -name "*.py" -mtime -1 -print0 |
  xargs -0 -I {} cp {} /tmp/recent/

# Parallel processing
find . -name "*.wav" -print0 |
  xargs -0 -P 8 -I {} ffmpeg -i {} {}.mp3
14

Продвинутые техники Bash

Here-документ и Here-строка

Here-документы (<< DELIMITER) передают многострочный текст как stdin команде — полезно для генерации файлов конфигурации, SQL-запросов или любого многострочного ввода. Некотируемый разделитель допускает раскрытие переменных; котируемый ('EOF') трактует содержимое буквально. Here-строки (<<<) передают одну строку как stdin — чище, чем echo | command для простых случаев. Разделителем может быть любое слово (EOF, END, DONE); по соглашению — в верхнем регистре. Here-документы необходимы для скриптов, генерирующих файлы или взаимодействующих с интерактивными программами (mysql, psql, ssh). Отступные разделители (<<-) удаляют ведущие табы для читаемости.

bash
# Here document: multi-line input to a command
cat << EOF
Line 1
Line 2 with $VARIABLE expansion
Line 3
EOF

# Quoted delimiter: no variable expansion
cat << 'EOF'
This $won't expand
\n stays literal
EOF

# Write to file
cat > config.txt << EOF
host=localhost
port=8080
EOF

# Here string: single string as stdin
grep "pattern" <<< "search this text"
read -r line <<< "hello world"

# With variables
name="Alice"
greeting=$(cat <<< "Hello, $name!")

# Pipe multi-line to command
mysql << EOF
USE mydb;
SELECT * FROM users;
EOF

Подстановка параметров

Подстановка параметров — встроенные строковые манипуляции Bash — не нужны sed/awk/cut для простых операций. :- предоставляет значения по умолчанию; # и ## удаляют префиксы; % и %% удаляют суффиксы. Примеры с расширениями файлов крайне распространены: ${file##*.} получает расширение, ${file%.*} — basename без расширения. / заменяет первое совпадение; // заменяет все. ^^ и ,, преобразуют регистр (Bash 4+). Эти операции быстрее порождения внешних команд. Освойте подстановку параметров для написания эффективных, читаемых Bash-скриптов без лишних subshell.

bash
# Default values
echo ${name:-"default"}    # use "default" if name unset/empty
echo ${name:="default"}    # set name to "default" if unset/empty

# String length
echo ${#var}

# Substring extraction
var="Hello World"
echo ${var:0:5}    # Hello (start:length)
echo ${var:6}      # World (start to end)

# Remove from beginning (# shortest, ## longest)
file="archive.tar.gz"
echo ${file#*.}     # tar.gz (remove up to first .)
echo ${file##*.}    # gz (remove up to last .)

# Remove from end (% shortest, %% longest)
echo ${file%.*}     # archive.tar (remove from last .)
echo ${file%%.*}    # archive (remove from first .)

# Replace
echo ${var/World/Bash}    # Hello Bash (first match)
echo ${var//l/L}          # HeLLo WorLd (all matches)

# Case conversion (Bash 4+)
echo ${var^^}   # HELLO WORLD (uppercase)
echo ${var,,}   # hello world (lowercase)

trap и обработка сигналов

trap регистрирует обработчики для сигналов и событий. EXIT срабатывает при завершении скрипта (нормальном, по ошибке или прерывании) — идеально для очистки (временные файлы, блокировки). INT (Ctrl-C), TERM (kill), HUP (закрытие терминала) — частые сигналы. Шаблон временных файлов (trap 'rm -f' EXIT) гарантирует очистку даже при сбое скрипта. DEBUG срабатывает перед каждой командой — полезно для трассировки. Всегда заключайте команду trap в кавычки (одинарные кавычки предотвращают немедленное раскрытие). trap необходим для надёжных скриптов: без него временные файлы накапливаются, а блокировки могут не сняться при сбое. Всегда убирайте за собой.

bash
# Clean up on exit (any reason)
cleanup() {
  echo "Cleaning up..."
  rm -f /tmp/myapp.lock
  exit 0
}
trap cleanup EXIT

# Handle specific signals
trap 'echo "Interrupted!"; cleanup' INT TERM
trap 'echo "Ctrl-C pressed"' INT

# Ignore a signal
trap '' INT  # ignore Ctrl-C

# Reset trap
trap - INT  # restore default behavior

# Common pattern: temp file cleanup
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
echo "data" > "$TMPFILE"
# ... use TMPFILE ...
# File is removed when script exits (even on error)

# Debug trap (trace execution)
trap 'echo "DEBUG: $BASH_COMMAND"' DEBUG

Отладка Bash-скриптов

set -x трассирует выполнение (выводит каждую команду с префиксом +) — основной инструмент отладки. set -euo pipefail (строгий режим) ловит ошибки рано: -e завершает при ненулевом возврате, -u ловит опечатки в именах переменных, pipefail делает сбои конвейеров корректными (без него учитывается только код завершения последней команды). bash -n проверяет синтаксис без выполнения. PS4 настраивает префикс трассировки (показ file:line очень полезен). trap ERR срабатывает при любой ошибке (с -e), предоставляя контекст номера строки. Для сложных скриптов добавляйте set -x в подозрительных секциях, а не глобально. Всегда используйте строгий режим в продакшен-скриптах.

bash
# Trace execution: print each command before running
bash -x script.sh
# Or within script:
set -x   # enable tracing
set +x   # disable tracing

# Verbose: print input lines as read
bash -v script.sh

# Strict mode (recommended for all scripts)
set -euo pipefail
# -e: exit on error
# -u: error on undefined variable
# -o pipefail: pipe fails if any command fails
# -o pipefail: pipe fails if any command fails

# Print specific debug info
echo "DEBUG: var=$var" >&2  # to stderr

# Check syntax without executing
bash -n script.sh

# PS4: customize trace prompt
export PS4='+${BASH_SOURCE}:${LINENO}: '
bash -x script.sh

# Trap ERR for error handling
trap 'echo "Error on line $LINENO"' ERR

curl и wget

curl и wget — необходимые HTTP-клиенты. curl предназначен для тестирования API (поддерживает все HTTP-методы, заголовки, JSON). -d отправляет POST-данные; -H устанавливает заголовки; -X указывает метод; -L следует за перенаправлениями; -O сохраняет с удалённым именем файла; -s тихий (для скриптинга). wget оптимизирован для скачивания (возобновление с -c, рекурсивность с -r). Для тестирования REST API стандарт — curl с -H 'Content-Type: application/json' и -d для JSON-тела. -w '%{http_code}' извлекает только код состояния для скриптинга. Для сложного тестирования API рассмотрите httpie (проще синтаксис) или postman.

bash
# Basic HTTP request
curl https://api.example.com/data
curl -O https://example.com/file.zip  # save to file

# HTTP methods
curl -X POST https://api.example.com/users
curl -X PUT -d '{"name":"Alice"}' https://api.example.com/users/1
curl -X DELETE https://api.example.com/users/1

# Headers and JSON
curl -H "Content-Type: application/json" \
     -H "Authorization: Bearer TOKEN" \
     -d '{"key":"value"}' \
     https://api.example.com/data

# Download with wget
wget https://example.com/file.zip
wget -q https://example.com/file.zip  # quiet
wget -c https://example.com/file.zip  # continue/resume

# Follow redirects
curl -L https://short.url/abc

# Save response, show headers
curl -s -o response.json -w "%{http_code}" https://api.example.com

# Download with progress bar
curl --progress-bar -O https://example.com/largefile.iso

# POST form data
curl -d "name=Alice&[email protected]" https://api.example.com/form
15

Сигналы и traps

обработчики очистки trap

trap регистрирует обработчики для сигналов. EXIT особенный — срабатывает при выходе из оболочки по любой причине, что делает его идеальным для очистки. Всегда очищайте lock-файлы, временные каталоги и дочерние процессы в trap. Частые сигналы для перехвата: INT (Ctrl-C), TERM (kill по умолчанию), HUP (закрытие терминала), EXIT.

bash
cleanup() {
  echo "Cleaning up..."
  rm -f /tmp/myapp.lock
  exit 0
}

# catch INT (Ctrl-C), TERM, EXIT
trap cleanup INT TERM EXIT

# normal exit also triggers EXIT trap
echo "Working..."
sleep 30
# cleanup runs automatically

# remove trap
trap - EXIT                      # disable
trap - INT TERM EXIT             # disable all

временные файлы с очисткой

Никогда не создавайте имена временных файлов вручную через $$ или $RANDOM — предсказуемые пути приглашают symlink-атаки. mktemp атомарно создаёт файл/каталог с уникальным именем. Сочетайте с trap на EXIT для гарантии очистки даже при ошибке или прерывании. Установите переменную окружения TMPDIR для управления расположением.

bash
TMPDIR=$(mktemp -d)
TMPFILE=$(mktemp)

cleanup() {
  rm -rf "$TMPDIR" "$TMPFILE"
}
trap cleanup EXIT

# use temp files safely
echo "data" > "$TMPFILE"
process_data < "$TMPFILE"

# mktemp is safer than $RANDOM — avoids symlink attacks
# -d creates a directory
# -p /var/tmp specifies parent
# mktemp -t myapp.XXXXXX   # use TMPDIR env

обработка сигналов в скриптах

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

bash
#!/bin/bash
# graceful shutdown for a long-running script

RUNNING=1
stop() {
  echo "Stopping..."
  RUNNING=0
}
trap stop INT TERM

while [ $RUNNING -eq 1 ]; do
  echo "Tick $(date)"
  sleep 1
done

echo "Done."

# send signals: kill -TERM <pid> or Ctrl-C

timeout и watch

timeout запускает команду и завершает её (SIGTERM по умолчанию) через заданное время. Код выхода 124 означает тайм-аут. Используйте --kill-after для эскалации до SIGKILL, если процесс игнорирует SIGTERM. Комбинируйте с циклами для построения проверок работоспособности. watch предназначен для человеко-ориентированного периодического отображения, а не для скриптов.

bash
# timeout kills a command after N seconds
timeout 30 slow_command
timeout 5s curl https://slow.example.com
timeout --signal=KILL 60 build.sh  # escalate to SIGKILL
timeout -k 5 30 command            # kill -9 after 5s grace

# exit status: 124 if timed out
timeout 2 sleep 10
echo $?                            # 124

# watch reruns a command
watch -n 1 date
watch -n 5 'kubectl get pods'

# tlimit-style loops
while true; do
  timeout 5 curl -s https://api.example.com/health || echo "down"
  sleep 60
done

игнорирование и повторная отправка

trap '' SIGNAL игнорирует сигнал (удобно для непрерываемых критических секций). Для пересылки сигналов дочернему процессу захватите его PID через $! и отправьте повторно. Trap ERR срабатывает при сбое любой команды при включённом set -e — полезно для логирования номера сбойной строки через $LINENO.

bash
# ignore a signal entirely
trap '' INT                       # ignore Ctrl-C
trap - INT                        # restore default

# re-raise / propagate to children
trap 'kill -TERM $childpid; wait $childpid' TERM

# forward signals to a subprocess
childpid=
run_child() {
  some_server &
  childpid=$!
  wait $childpid
}
trap 'kill -TERM $childpid' TERM INT
run_child

# exit on any error
set -e
trap 'echo "Failed at line $LINENO"' ERR
16

Функции и библиотеки

определение функций

Функции группируют переиспользуемые команды. Два синтаксиса: name() {...} и function name {...}. Аргументы внутри функций — $1, $2 и т. д. — $0 остаётся именем скрипта. $@ раскрывается во все аргументы (всегда заключайте в кавычки как "$@" для сохранения аргументов с пробелами). $# — количество.

bash
# two equivalent syntaxes
greet() {
  echo "Hello, $1!"
}

function greet {                  # also valid
  echo "Hello, $1!"
}

greet Alice                       # Hello, Alice!
greet "Bob Smith"                 # quoted args

# arguments: $1, $2, ... $@, $#, $*
echo_args() {
  echo "Count: $#"
  echo "All: $@"
  echo "First: $1"
  for arg in "$@"; do echo "- $arg"; done
}

возвращаемые значения и локальные переменные

return устанавливает статус завершения (0=успех, 1-255=неудача) — он НЕ возвращает значение. Чтобы получить значение обратно, выведите его через echo и захватите через $(). Используйте local для переменных внутри функций во избежание загрязнения глобальной области. local необходим в рекурсивных функциях.

bash
# return is exit status (0-255), not a value
is_even() {
  if (( $1 % 2 == 0 )); then return 0; else return 1; fi
}

if is_even 4; then echo "even"; fi

# capture stdout as a value
get_date() { date +%Y-%m-%d; }
today=$(get_date)

# local variables
counter() {
  local count=0                  # scoped to function
  ((count++))
  echo $count
}

подключение библиотек

source (или .) выполняет файл в текущей оболочке — поэтому функции, переменные и алиасы становятся доступными. Так вы строите переиспользуемые библиотеки. BASH_SOURCE[0] vs $0 различает подключённый vs выполненный файл — удобно для файлов, являющихся одновременно библиотекой и исполняемым скриптом.

bash
# library file: lib/utils.sh
log() {
  echo "[$(date +%H:%M:%S)] $*" >&2
}

CONFIG_PATH="/etc/myapp"

# main script
source lib/utils.sh              # or: . lib/utils.sh
log "Starting..."
echo "Config: $CONFIG_PATH"

# check if sourced vs executed
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  echo "Running directly"
else
  echo "Sourced"
fi

рекурсивные функции

Bash поддерживает рекурсию, но она медленная (каждый вызов порождает subshell для $()) с ограниченной глубиной стека (~1000). Для вычислительно тяжёлой работы предпочитайте awk, Python или внешние инструменты. Всегда используйте local-переменные в рекурсивных функциях — иначе они затирают друг друга между вызовами.

bash
# factorial
factorial() {
  local n=$1
  if (( n <= 1 )); then
    echo 1
  else
    local prev=$(factorial $((n - 1)))
    echo $((n * prev))
  fi
}
factorial 5                      # 120

# fibonacci
fib() {
  if (( $1 < 2 )); then echo $1; return; fi
  echo $(( $(fib $(($1 - 1))) + $(fib $(($1 - 2))) ))
}

# caution: bash recursion is slow and limited depth
# for heavy work, use awk, python, or external tools

аргументы по умолчанию и необязательные

${1:-default} подставляет значение по умолчанию, если $1 не установлен или пуст. ${1:?message} завершается с ошибкой, если отсутствует — отлично для обязательных аргументов. shift извлекает $1 из списка аргументов. Комбинация case + shift — идиоматичный способ разбора флагов и позиционных аргументов в bash-функциях.

bash
# default values
greet() {
  local name="${1:-World}"        # default if unset or empty
  local greeting="${2:-Hello}"
  echo "$greeting, $name!"
}
greet                            # Hello, World!
greet Alice                      # Hello, Alice!
greet Alice Hi                   # Hi, Alice!

# required argument
require_arg() {
  : "${1:?Usage: require_arg <name>}"   # exits with message if missing
  echo "Got: $1"
}

# shift through args
process() {
  while (( $# )); do
    case "$1" in
      -v) verbose=1; shift ;;
      *)  files+=("$1"); shift ;;
    esac
  done
}
17

Массивы и ассоциативные массивы

индексные массивы

Индексные массивы используют целочисленные ключи с нуля. Всегда заключайте "${arr[@]}" в кавычки при итерации для корректной обработки элементов с пробелами. ${#arr[@]} — количество. ${!arr[@]} возвращает индексы (полезно для разреженных массивов). unset 'arr[i]' (в кавычках) удаляет элемент.

bash
# declare and assign
fruits=("apple" "banana" "cherry")
fruits+=("date")                 # append
fruits[10]="kiwi"                # sparse

# access
echo "${fruits[0]}"             # first
echo "${fruits[@]}"             # all values
echo "${#fruits[@]}"            # count
echo "${!fruits[@]}"            # indices

# iterate
for f in "${fruits[@]}"; do
  echo "$f"
done

# slice
echo "${fruits[@]:1:2}"         # 2 items starting at index 1

# delete
unset 'fruits[1]'

ассоциативные массивы (map)

Ассоциативные массивы (declare -A) — возможность bash 4+ (в macOS по умолчанию bash 3 — используйте brew install bash). Ключи — строки. Проверка -v определяет, существует ли ключ. Итерация ключей через ${!arr[@]}. В macOS по умолчанию /bin/bash версии 3.2 — скриптам, использующим -A, нужен #!/usr/bin/env bash, указывающий на более новый bash.

bash
# requires bash 4+
declare -A ages
ages["alice"]=30
ages["bob"]=25
ages["carol"]=42

# access
echo "${ages["alice"]}"
echo "${ages[@]}"               # all values
echo "${!ages[@]}"              # all keys

# iterate keys
for name in "${!ages[@]}"; do
  echo "$name => ${ages[$name]}"
done

# check key exists
if [[ -v ages["alice"] ]]; then echo "yes"; fi

# count
echo "${#ages[@]}"

# delete
unset 'ages[bob]'

чтение в массивы

mapfile (он же readarray) — самый быстрый способ считать строки в массив — всегда используйте -t для удаления завершающих переводов строк. Цикл while-read переносим, но медленнее. Для разделения строки с разделителями установите IFS и используйте read -ra. Подстановка процесса < <(cmd) избегает проблем с областью видимости переменных subshell.

bash
# read lines into array
mapfile -t lines < file.txt      # bash 4+
echo "${lines[0]}"

# alternative (portable)
lines=()
while IFS= read -r line; do
  lines+=("$line")
done < file.txt

# split string into array
IFS=',' read -ra csv <<< "a,b,c,d"
echo "${csv[2]}"                # c

# from command
mapfile -t files < <(find . -name "*.js")

# process each
for f in "${files[@]}"; do
  echo "Processing $f"
done

операции с массивами

Массивы Bash лишены многих встроенных операций (нет .indexOf, .reverse, .unique). Для этого пишите циклы или передавайте через sort/uniq. Трюк unique-массива использует ассоциативный массив как множество. Для сложной манипуляции массивами рассмотрите awk или настоящий язык программирования — массивы bash лучше всего подходят для простых списков.

bash
# append
arr=(a b c)
arr+=(d e)                       # (a b c d e)

# length
echo "${#arr[@]}"

# slice
echo "${arr[@]:1:2}"            # (b c)

# find index (no built-in; loop)
idx=-1
for i in "${!arr[@]}"; do
  if [[ "${arr[$i]}" == "b" ]]; then idx=$i; break; fi
done

# reverse
n=${#arr[@]}
for ((i=n-1; i>=0; i--)); do
  echo "${arr[$i]}"
done

# unique (preserve order)
declare -A seen
unique=()
for x in "${arr[@]}"; do
  [[ -v seen[$x] ]] && continue
  seen[$x]=1
  unique+=("$x")
done

вложенные данные с массивами

В Bash нет встроенной поддержки вложенных массивов. Обходные пути: (1) храните записи как строки, объединённые разделителями, и разделяйте через IFS; (2) используйте ассоциативный массив с составными ключами вида "r,c"; (3) используйте косвенность переменных через ${!name}. Для настоящих вложенных данных переключитесь на jq + JSON или настоящий язык.

bash
# array of "records" (delimiter-based)
users=("alice:30:admin" "bob:25:user")
for u in "${users[@]}"; do
  IFS=':' read -r name age role <<< "$u"
  echo "$name ($age) - $role"
done

# simulate 2D with name-mangled vars
declare -A grid
grid["0,0"]=1; grid["0,1"]=2
grid["1,0"]=3; grid["1,1"]=4
for key in "${!grid[@]}"; do
  IFS=',' read -r r c <<< "$key"
  echo "[$r,$c] = ${grid[$key]}"
done

# arrays of arrays (not directly supported)
# workaround: use indirection
rows=("row0" "row1")
row0=(a b c)
row1=(d e f)
ref="${rows[0]}[@]"
echo "${!ref}"                   # a b c
18

Манипуляции со строками

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

Подстановка параметров — самый мощный строковый инструмент bash. # и ## удаляют спереди (кратчайшее/длиннейшее совпадение); % и %% удаляют с конца. / заменяет первое, // заменяет все. Это избегает порождения subshell — намного быстрее, чем конвейер через sed для простых операций.

bash
path="/var/log/app.log"

# length
echo "${#path}"                 # 16

# substring (offset:length)
echo "${path:5}"                # log/app.log
echo "${path:5:3}"              # log

# remove from front (shortest/longest)
echo "${path#*/}"               # var/log/app.log
echo "${path##*/}"              # app.log (basename)

# remove from end
echo "${path%/*}"               # /var/log (dirname)
echo "${path%%.*}"              # /var/log/app

# replace
echo "${path/log/LOG}"          # /var/LOG/app.log (first)
echo "${path//log/LOG}"         # all occurrences

преобразование регистра

Bash 4+ добавил ,, (нижний), ^^ (верхний), ^ (капитализировать первый символ), , (нижний первый символ). declare -u/-l авто-преобразует присваивания. Для старого bash (по умолчанию в macOS) используйте tr или awk. Преобразование регистра часто используется при нормализации пользовательского ввода или построении идентификаторов.

bash
s="Hello World"

# bash 4+
echo "${s,,}"                   # hello world (lower)
echo "${s^^}"                   # HELLO WORLD (upper)
echo "${s^}"                    # Hello World (capitalize first)
echo "${s,}"                    # hello World (lowercase first)

# pattern-based
echo "${s^^[aeiou]}"            # hEllO wOrld (vowels upper)

# portable (pre-bash 4)
echo "$s" | tr 'a-z' 'A-Z'
echo "$s" | awk '{print toupper($0)}'

# declare typed variables
declare -u UPPER="hello"         # HELLO
declare -l LOWER="WORLD"         # world

разделение и объединение

Разделение: установите IFS и используйте read -ra. Объединение: ${arr[*]} с установленным IFS работает, но капризно — функция join_by надёжнее. Трюк printf '%s' "$d%s" добавляет разделитель перед каждым оставшимся аргументом, создавая чистое объединение. tr может преобразовывать между разделителями и переводами строк.

bash
# split on delimiter
csv="a,b,c,d"
IFS=',' read -ra parts <<< "$csv"
echo "${parts[2]}"              # c
for p in "${parts[@]}"; do echo "$p"; done

# join with delimiter (bash 4+)
arr=(a b c d)
joined=$(IFS=,; echo "${arr[*]}")
echo "$joined"                   # a,b,c,d

# custom joiner
join_by() {
  local d=$1; shift
  local first=$1; shift
  printf '%s' "$first"
  printf '%s' "$d%s" "$@"
}
join_by " | " a b c              # a | b | c

# split into lines
tr ',' '\n' <<< "a,b,c"

поиск и проверка

Сопоставление шаблонов: == с подстановочными знаками (* ? [..]) выполняет glob-сопоставление. =~ выполняет расширенный regex; группы захвата попадают в BASH_REMATCH. shopt -s nocasematch делает сравнения в [[ ]] нечувствительными к регистру. expr устарел — предпочитайте [[ ]] для нового кода.

bash
s="Hello World"

# substring search
if [[ "$s" == *"World"* ]]; then echo "contains"; fi
if [[ "$s" == Hello* ]]; then echo "starts with"; fi
if [[ "$s" == *World ]]; then echo "ends with"; fi

# regex match
if [[ "$s" =~ ^H[a-z]+\sW[a-z]+$ ]]; then echo "match"; fi
echo "${BASH_REMATCH[0]}"       # whole match
echo "${BASH_REMATCH[1]}"       # first group

# case-insensitive
shopt -s nocasematch
[[ "$s" == *world* ]] && echo "yes"
shopt -u nocasematch

# index (no built-in; use expr)
expr "$s" : ".*World"            # length up to match

форматирование printf

printf мощнее echo — поддерживает C-стиль спецификаторов формата (%s, %d, %f, %x), ширину/точность и выравнивание. %()T форматирует время без порождения команды date. printf -v сохраняет результат в переменной. printf '=%.0s' {1..40} — изящный трюк повторить символ N раз.

bash
# basic
printf '%s\n' "hello" "world"   # one per line
printf '%s = %d\n' "count" 42

# width and padding
printf '%10s\n' "right"         # right-aligned, width 10
printf '%-10s|\n' "left"        # left-aligned
printf '%05d\n' 42              # 00042 (zero-padded)
printf '%.2f\n' 3.14159         # 3.14

# date formatting
printf 'Today: %(%Y-%m-%d)T\n' -1

# to variable
printf -v today '%(%Y%m%d)T' -1
echo "$today"                    # 20260621

# repeat
printf '=%.0s' {1..40}; echo     # 40 equals signs
19

Регулярные выражения

bash regex с =~

=~ использует ERE (расширенный regex). Группы захвата заполняют BASH_REMATCH (индекс 0 = полное совпадение, 1+ = группы). Заключайте строку в кавычки, но НЕ regex (закавычивание regex делает его литеральным). Для сопоставления без учёта регистра используйте shopt -s nocasematch. Regex — POSIX ERE — без \d, \w; используйте [0-9], [A-Za-z0-9_].

bash
s="user_42"

if [[ "$s" =~ ^([a-z]+)_([0-9]+)$ ]]; then
  echo "Match!"
  echo "Name: ${BASH_REMATCH[1]}"   # user
  echo "ID: ${BASH_REMATCH[2]}"     # 42
fi

# email-ish check
email="[email protected]"
if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
  echo "valid"
fi

# case-insensitive
shopt -s nocasematch
[[ "$s" =~ USER ]] && echo "yes"
shopt -u nocasematch

шаблоны grep regex

grep по умолчанию использует BRE (базовый regex), где +, ?, |, () требуют обратной косой черты. -E включает ERE (чище синтаксис). -P включает PCRE с \d, \w, \b, lookarounds — наиболее мощный, но менее переносимый (только GNU grep). Для скриптов, которые должны работать на macOS/BSD, предпочитайте -E.

bash
# basic (BRE) — default
grep 'foo' file
grep 'a\.' file                 # literal dot (escape in BRE)
grep 'a*b' file                  # zero or more a

# extended (ERE) — -E
grep -E 'a+b' file               # one or more
grep -E 'a|b' file               # alternation
grep -E '(ab)+' file             # grouping
grep -E '[0-9]{3}' file          # quantifier

# perl (PCRE) — -P
grep -P '\d{3}' file            # digits
grep -P '\bword\b' file        # word boundary
grep -P '(?<=foo)bar' file       # lookbehind

# anchors
grep -E '^Error' file            # line start
grep -E 'done$' file             # line end
grep -E '^Error.*timeout$' file

подстановка в sed regex

sed по умолчанию использует BRE (группам захвата нужны \( \), обратные ссылки \1). -E переключает на ERE (чище () и +, ?, |). Флаг I делает подстановку нечувствительной к регистру (GNU sed). Используйте другой разделитель (s|...|...|), когда шаблон содержит слэши. [[:space:]] переносимо для пробельных символов.

bash
# BRE (default)
sed 's/foo/bar/' file
sed 's/a\.b/x/' file            # literal dot
sed 's/a\(b\)c/\1/' file      # capture with backrefs

# ERE with -E
sed -E 's/a+b/x/' file
sed -E 's/(foo|bar)/X/g' file
sed -E 's/([0-9]{4})/Year: \1/' file

# case-insensitive
sed 's/foo/bar/I' file
sed 's/[fF][oO][oO]/bar/g' file

# common recipes
sed -E 's/^[[:space:]]+//' file  # trim leading whitespace
sed -E 's/[[:space:]]+$//' file  # trim trailing
sed -E 's/ +/ /g' file           # collapse spaces
sed -E 's#https?://[^ ]+##g' file # strip URLs

awk regex

awk использует ERE. Оператор ~ проверяет строку на regex. /pat1/, /pat2/ — шаблон диапазона (совпадает с pat1 по pat2 включительно). gsub выполняет глобальную подстановку на месте. match() в gawk с третьим аргументом захватывает группы в массив — удобно, но непереносимо в mawk/POSIX awk.

bash
# match operator ~
awk '$1 ~ /^[0-9]+/' file        # field 1 is numeric
awk '$0 !~ /debug/' file         # line doesn't contain
awk '/^Error/,/^$/' file         # from Error to blank line

# case-insensitive
awk 'BEGIN { IGNORECASE=1 } /error/' file
awk 'tolower($0) ~ /error/' file

# gsub (global substitute)
awk '{ gsub(/foo/, "bar"); print }' file
awk '{ gsub(/[0-9]+/, "N"); print }' file

# match with capture (gawk)
awk 'match($0, /id=([0-9]+)/, m) { print m[1] }' file

# split on regex
awk '{ n = split($0, parts, /[,;\t]/); print n }' file

частые рецепты regex

Эти рецепты покрывают типичные задачи извлечения текста. -o выводит только совпавшую часть. Учтите, что regex не идеален для парсинга HTML, JSON или XML — используйте соответствующие парсеры (jq для JSON, xmllint для XML). Для IPv4 этот regex соответствует формату, но не валидирует диапазоны (255.255.255.255 vs 999.999.999.999).

bash
# IPv4
grep -E '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log

# URL
grep -Eo 'https?://[^[:space:]]+' file

# email
grep -Eo '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' file

# date YYYY-MM-DD
grep -E '[0-9]{4}-[0-9]{2}-[0-9]{2}' file

# hex color
grep -Eo '#[0-9A-Fa-f]{6}' file

# strong password (8+, upper, lower, digit)
grep -E '^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{8,}$' file

# trim whitespace
sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' file

# extract domain from URL
sed -E 's#https?://([^/]+).*#\1#' file
20

Отладка

опции set

set -e завершает при любом сбое команды (обязательно для скриптов). set -u ловит опечатки в именах переменных. set -o pipefail делает конвейер сбойным при сбое любой части (без него учитывается только код завершения последней команды). set -x трассирует выполнение. Объединённые как -euo pipefail, это 'строгий режим' — безопасное значение по умолчанию.

bash
#!/bin/bash
set -e                           # exit on error
set -u                           # error on unset variable
set -o pipefail                  # pipe fails if any command fails
set -x                           # print commands (trace)
# or combined:
set -euo pipefail

# shorthand
set -Eeuxo pipefail              # -E: ERR trap inherits

# strict mode for robust scripts
set -euo pipefail
IFS=$'\n\t'

# trace specific section
set -x
complex_logic
set +x

# debug a script
bash -x script.sh
bash -xv script.sh               # verbose + trace

трассировка с -x

set -x выводит каждую команду перед выполнением с + как префиксом по умолчанию (PS4). Кастомизация PS4 для включения file:line:function делает трассировки намного полезнее для отладки. BASH_XTRACEFD перенаправляет трассировку на другой FD, позволяя отделить вывод трассировки от stdout/stderr.

bash
# trace entire script
bash -x script.sh

# trace inside script
set -x
echo "tracing on"
set +x

# customize PS4 (the trace prompt)
export PS4='+ ${BASH_SOURCE:-shell}:${LINENO}: ${FUNCNAME[0]:-main}() '
set -x
my_function
set +x

# trace to file
exec 5> /tmp/trace.log
BASH_XTRACEFD=5
set -x
# ... commands ...
set +x
exec 5>&-

обработка ошибок и trap ERR

Trap ERR срабатывает при сбое команды (с set -e). $? внутри trap возвращает код сбоя. $LINENO показывает место сбоя. Встроенная caller выводит file:line:function — циклический вызов даёт трассировку стека. set -E заставляет trap ERR срабатывать и внутри функций (иначе он отключён в контексте функции).

bash
set -e
set -E                           # ERR trap inherits functions

on_error() {
  local exit_code=$?
  local line=$1
  echo "ERROR: command failed at line $line with exit code $exit_code" >&2
  echo "Stack:" >&2
  local i=0
  while caller $i >&2; do ((i++)); done
  exit $exit_code
}
trap 'on_error $LINENO' ERR

# now any failing command triggers the handler
false                            # triggers on_error

# caller builtin gives file:line:func
# stack trace via: while caller $i; do ((i++)); done

линтер shellcheck

shellcheck — де-факто линтер для shell-скриптов — ловит проблемы с кавычками, неиспользуемые переменные, частые ловушки и предлагает идиомы. Запускайте его для каждого скрипта. Самое частое исправление — SC2086: заключайте переменные в кавычки для предотвращения разбиения на слова и globbing. Многие CI-конвейеры требуют успешного прохождения shellcheck.

bash
# install: apt install shellcheck (or brew, or use shellcheck.net)
# run:
shellcheck script.sh
shellcheck -x script.sh          # trust includes (no SC1090 warnings)
shellcheck -S warning script.sh  # only warnings+
shellcheck --exclude=SC2086 *.sh # exclude specific rules

# common warnings:
# SC2086: double-quote to prevent globbing (use "$var")
# SC2046: quote $(cmd) to prevent word splitting
# SC2034: unused variable
# SC2155: declare and assign separately (masks return code)
# SC2004: $/${} unnecessary in arithmetic

# in script: disable inline
# shellcheck disable=SC2086
echo $unquoted_var

# disable for next line
# shellcheck disable=SC2086
echo $args

логирование и диагностика

Логгер с уровнями (DEBUG/INFO/WARN/ERROR), временными метками и цветами значительно упрощает отладку скриптов. Всегда пишите логи в stderr, чтобы stdout оставался чистым для данных. Поддерживайте флаг --verbose (включает set -x) и --dry-run (выводит команды без выполнения). ANSI-коды цветов: 31=красный, 33=жёлтый, 32=зелёный, 90=серый.

bash
# logging function with levels
log() {
  local level=$1; shift
  local msg="$*"
  local ts=$(date '+%Y-%m-%d %H:%M:%S')
  local color
  case $level in
    DEBUG) color='\033[90m' ;;
    INFO)  color='\033[32m' ;;
    WARN)  color='\033[33m' ;;
    ERROR) color='\033[31m' ;;
  esac
  printf "%b[%s] [%s] %s\033[0m\n" "$color" "$ts" "$level" "$msg" >&2
}

log INFO "Starting process"
log WARN "Disk space low"
log ERROR "Connection failed"

# verbose flag
[[ $VERBOSE ]] && set -x

# dry-run mode
if [[ $DRY_RUN ]]; then
  echo "Would: $cmd"
else
  eval "$cmd"
fi
21

Безопасность

кавычки и инъекции

Главное правило безопасности bash: заключайте каждое раскрытие переменной в кавычки. Незакавыченный $var подвергается разбиению на слова и globbing, что приводит к багам и инъекциям. Для команд с динамическими аргументами используйте массив (cmd=(ls "$dir"); "${cmd[@]}") вместо строки. Никогда не передавайте пользовательский ввод в eval или $() как строку оболочки.

bash
# ALWAYS quote variables
file="my file.txt"
rm $file                         # BAD: rm my file.txt (3 args!)
rm "$file"                       # GOOD: rm "my file.txt"

# use arrays for command + args
files=("a.txt" "b file.txt" "c.txt")
rm "${files[@]}"                # each file as separate arg, safely

# avoid eval
eval "echo $user_input"          # DANGEROUS
echo "$user_input"               # safe

# avoid command substitution in unsafe places
# BAD: filename from user, executed
cmd="ls $userdir"
$cmd                             # word-splitting + globbing

# GOOD: use array
cmd=(ls "$userdir")
"${cmd[@]}"

безопасные временные файлы и секреты

mktemp создаёт файлы с непредсказуемыми именами и правами 600 — безопасно от symlink-гонок. Всегда очищайте через trap. Читайте секреты из переменных окружения, никогда не хардкодьте. Избегайте передачи секретов как аргументов командной строки (видимы через ps) — используйте stdin, переменные окружения или файл конфигурации с правами 600.

bash
# safe temp files
tmp=$(mktemp)                    # atomic, unpredictable name
trap 'rm -f "$tmp"' EXIT

# safe temp dir
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

# secrets: read from env, never hardcode
api_key="${API_KEY:?Set API_KEY env var}"

# don't leak secrets to process list (ps)
# BAD: curl -H "Authorization: Bearer $TOKEN" ...
# GOOD: pass via stdin or config file
curl -H @- <<EOF https://api.example.com
Authorization: Bearer $TOKEN
EOF

# restrict file perms for secrets
umask 077
echo "$api_key" > ~/.secrets/api
chmod 600 ~/.secrets/api

валидация ввода

Валидируйте весь пользовательский ввод перед использованием. Regex проверяет формат; арифметика — диапазоны. Для путей файлов отклоняйте .. (обход каталогов) и абсолютные пути, если они явно не разрешены. tr -cd удаляет символы не из разрешённого набора — полезно для санитизации идентификаторов. Белый список (case) безопаснее чёрного списка для разрешённых действий.

bash
# validate numeric input
read -p "Enter a number: " n
if ! [[ "$n" =~ ^[0-9]+$ ]]; then
  echo "Not a number" >&2; exit 1
fi

# validate range
if (( n < 1 || n > 100 )); then
  echo "Out of range" >&2; exit 1
fi

# validate file path (no traversal)
path="$1"
case "$path" in
  *..*|/*) echo "Invalid path" >&2; exit 1 ;;
esac

# sanitize filename
safe=$(echo "$name" | tr -cd 'A-Za-z0-9._-')

# whitelist allowed values
case "$action" in
  start|stop|restart) ;;
  *) echo "Invalid action" >&2; exit 1 ;;
esac

сброс привилегий и setuid

setuid-скрипты оболочки небезопасны — большинство систем игнорируют setuid-бит на скриптах. Используйте sudo с жёсткой записью sudoers (конкретная команда, без пароля). Сбрасывайте root как можно раньше через exec sudo -u. Для возможностей (например, привязка порта 80) используйте setcap вместо запуска от root. chmod 700 сохраняет скрипты приватными для владельца.

bash
# drop privileges in a script running as root
if [[ $EUID -eq 0 ]]; then
  exec sudo -u appuser "$0" "$@"
fi

# check for root
if [[ $EUID -ne 0 ]]; then
  echo "Requires root" >&2; exit 1
fi

# setuid scripts are DANGEROUS — avoid them
# instead, use sudoers entries
# /etc/sudoers.d/myapp:
#   appuser ALL=(root) NOPASSWD: /usr/local/bin/myapp.sh

# restrict script permissions
chmod 700 script.sh              # only owner can run
chown appuser:appuser script.sh

# use capsh instead of full root for fine-grained caps
# setcap 'cap_net_bind_service=+ep' /usr/bin/myapp

подписи и целостность

Всегда проверяйте контрольные суммы и подписи для скачанных скриптов, особенно инсталляторов, запускаемых от root. sha256sum -c сравнивает с известным хорошим хэшем. GPG-подписи доказывают и целостность, и подлинность (идентичность подписавшего). Передача curl по конвейеру в sha256sum позволяет просмотреть хэш перед выполнением — но безопаснее шаблон скачать-затем-проверить-затем-выполнить.

bash
# verify checksum
sha256sum -c file.sha256
# file.sha256 contains: <hash>  <filename>

# generate checksums
sha256sum release.tar.gz > release.tar.gz.sha256
sha256sum *.iso > isos.sha256

# GPG sign and verify
gpg --sign file.txt              # creates file.txt.gpg
gpg --verify file.txt.gpg
gpg --detach-sig --armor file.tar.gz
gpg --verify file.tar.gz.asc file.tar.gz

# download + verify in one go
curl -fsSL https://example.com/install.sh | sha256sum
curl -fsSL https://example.com/install.sh -o install.sh
sha256sum -c <(echo "expected_hash  install.sh")
22

Сеть

curl, продвинутое

curl передаёт данные через URL. -X устанавливает метод, -d отправляет данные, -H устанавливает заголовки. -O сохраняет с удалённым именем файла. -I получает только заголовки. -L следует за перенаправлениями. -u user:pass для аутентификации. -v для подробного вывода.

bash
# POST with data
curl -X POST -d "name=Alice" https://api.example.com
# JSON POST
curl -H "Content-Type: application/json" \
     -d '{"name":"Alice"}' https://api.example.com
# Download with progress
curl -O https://example.com/file.zip
# Headers only
curl -I https://example.com

wget

wget скачивает файлы неинтерактивно. -m зеркалирует сайты. -c возобновляет прерванные загрузки. -r рекурсивно обходит, -l ограничивает глубину. -b работает в фоне. Лучше curl для рекурсивных загрузок.

bash
# Download file
wget https://example.com/file.zip
# Mirror website
wget -m https://example.com
# Resume download
wget -c https://example.com/largefile.zip
# Recursive download
wget -r -l 2 https://example.com
# Background
wget -b https://example.com/file.zip

ssh и scp

ssh подключается к удалённым машинам. -i указывает файл ключа. -L создаёт локальное перенаправление портов. scp копирует файлы через SSH. -r рекурсивно обходит каталоги. Используйте ssh-copy-id для установки ключей. Настройте ~/.ssh/config для алиасов.

bash
# SSH connect
ssh user@host
# SSH with key
ssh -i ~/.ssh/id_rsa user@host
# Port forwarding
ssh -L 8080:localhost:80 user@host
# Copy file
scp file.txt user@host:/path/
# Copy directory
scp -r dir/ user@host:/path/

netstat и ss

netstat и ss показывают сетевые соединения. -t TCP, -u UDP, -l прослушивание, -n числовой, -p процесс. ss быстрее и детальнее netstat. Используйте для поиска процесса, занимающего порт, или диагностики проблем с соединениями.

bash
# List listening ports
netstat -tlnp
ss -tlnp
# Show all connections
netstat -an
ss -tunap
# Show routing table
netstat -r
# Show interface stats
netstat -i

ping и traceroute

ping проверяет достижимость и задержку (-c ограничивает количество). traceroute показывает путь до хоста. dig запрашивает DNS-записи (A, MX, NS, TXT). +short даёт краткий вывод. Используйте для сетевой диагностики и устранения неполадок DNS.

bash
# Check connectivity
ping -c 4 google.com
# Trace route
traceroute google.com
# DNS lookup
dig example.com
dig +short example.com
# Get DNS records
dig MX example.com
dig NS example.com
23

Углублённый скриптинг оболочки

Функции

Функции группируют команды. local создаёт переменные, ограниченные функцией. $1, $2 — аргументы. return устанавливает статус завершения (0-255). Захватывайте вывод через $(). Функции должны быть определены до использования. Используйте source для загрузки из файлов.

bash
greet() {
    local name="$1"
    echo "Hello, $name"
    return 0
}
greet "Alice"
# Return value via $?
result=$(greet "Bob")

Условные операторы

[ ] — команда test. -f файл, -d каталог, -z пустая строка, -n непустая. = для строк, -eq/-ne/-gt/-lt для чисел. Всегда заключайте переменные в кавычки для обработки пробелов. [[ ]] — расширенная версия bash с regex.

bash
if [ -f "/path/file" ]; then
    echo "File exists"
elif [ -d "/path" ]; then
    echo "Directory exists"
else
    echo "Not found"
fi
# String comparison
if [ "$str" = "hello" ]; then ... fi
# Numeric
if [ "$num" -gt 10 ]; then ... fi

Циклы

for перебирает списки. C-стиль for использует (( )). while читает, пока условие не нарушится. while read безопасно обрабатывает файлы построчно. Всегда заключайте переменные в кавычки. Используйте find для рекурсивной итерации файлов.

bash
# For loop
for i in 1 2 3; do
    echo $i
done
# C-style
for ((i=0; i<5; i++)); do echo $i; done
# While loop
while read line; do
    echo "$line"
done < file.txt
# Iterate files
for f in *.txt; do process "$f"; done

Массивы

Массивы используют круглые скобки. ${arr[@]} раскрывает все элементы. ${#arr[@]} — длина. Всегда заключайте "${arr[@]}" в кавычки для обработки пробелов. declare -A создаёт ассоциативные массивы (bash 4+). Используйте read -a для разделения в массив.

bash
# Declare array
fruits=("apple" "banana" "cherry")
# Access
echo ${fruits[0]}      # apple
echo ${fruits[@]}      # all
echo ${#fruits[@]}     # length
# Iterate
for fruit in "${fruits[@]}"; do
    echo "$fruit"
done
# Associative array
declare -A config
config[key]="value"

Обработка ошибок

set -e завершает при ошибке, -u при неопределённых переменных, pipefail ловит сбои конвейеров. trap ERR обрабатывает ошибки. trap EXIT запускает очистку. command -v проверяет, существует ли команда. Всегда начинайте скрипты с set -euo pipefail.

bash
set -e  # Exit on error
set -u  # Error on undefined variable
set -o pipefail  # Pipeline fails if any command fails
set -euo pipefail  # All three (recommended)
trap 'echo "Error on line $LINENO"' ERR
trap 'cleanup' EXIT
# Check command result
if ! command -v docker &> /dev/null; then
    echo "docker not installed"
fi
24

Углублённые операции с файлами

find, продвинутое

find ищет файлы по критериям. -type f/d для файлов/каталогов. -size +1M больше 1 МБ. -mtime -7 изменён в течение 7 дней. -exec запускает команду для каждого совпадения. {} — имя файла; \; завершает команду.

bash
# By name
find . -name "*.txt"
# By type and size
find . -type f -size +1M
# By modification time
find . -mtime -7  # Modified in last 7 days
# Execute command
find . -name "*.log" -exec gzip {} \;
# Delete
find . -name "*.tmp" -delete

tar и сжатие

tar объединяет файлы; -z сжимает gzip. -c создать, -x извлечь, -t список, -v подробно, -f имя файла. Для .tar.bz2 используйте -j. zip/unzip обрабатывают формат ZIP. Используйте -C для распаковки в конкретный каталог.

bash
# Create tar.gz
tar -czvf archive.tar.gz dir/
# Extract
tar -xzvf archive.tar.gz
# List contents
tar -tzvf archive.tar.gz
# zip/unzip
zip -r archive.zip dir/
unzip archive.zip

rsync

rsync эффективно синхронизирует файлы, передавая только различия. -a архивный режим (сохраняет атрибуты), -v подробно, -z сжимать. --delete удаляет файлы, отсутствующие в источнике. Завершающий слэш в src/ важен: с ним копируется содержимое; без него — каталог.

bash
# Sync local
rsync -av src/ dest/
# Sync to remote
rsync -avz src/ user@host:/path/
# Delete files not in source
rsync -av --delete src/ dest/
# Dry run
rsync -av --dry-run src/ dest/
# Exclude
rsync -av --exclude='*.log' src/ dest/

права файлов

Права: r=4, w=2, x=1. 755 = rwxr-xr-x (владелец полный, остальные чтение/выполнение). 644 = rw-r--r-- (файлы). u/g/o/a = user/group/other/all. chmod -R рекурсивно. chown изменяет владельца.

bash
# Symbolic
chmod u+x file      # User execute
chmod g-w file      # Group write off
chmod a=r file      # All read only
# Numeric
chmod 755 file      # rwxr-xr-x
chmod 644 file      # rw-r--r--
# Change owner
chown user:group file
# Recursive
chmod -R 755 dir/

ln и символьные ссылки

Жёсткие ссылки указывают на тот же inode (только в пределах одной файловой системы). Символьные ссылки (-s) указывают на путь. Жёсткие ссылки переживают удаление оригинала. Symlinks ломаются при перемещении цели. Используйте readlink для разрешения символьных ссылок.

bash
# Hard link
ln file.txt hardlink.txt
# Symbolic link
ln -s /path/to/target symlink
# Find broken symlinks
find . -type l ! -exec test -e {} \; -print
# Copy symlink target
cp -L symlink dest/
25

Частые ловушки

Незакавыченные переменные

Незакавыченные переменные разбиваются по пробелам и glob. $(ls) ломается на именах файлов с пробелами. Используйте glob (*.txt) или find -print0 с read -d. Всегда заключайте переменные в кавычки: "$var". Используйте IFS= read -r для безопасного чтения строк.

bash
# BUG: breaks with spaces
for f in $(ls); do echo "$f"; done
# GOOD: handle spaces
for f in *.txt; do echo "$f"; done
# Or with find
find . -name "*.txt" -print0 | while IFS= read -r -d '' f; do
    echo "$f"
done

cp vs mv

cp копирует, mv перемещает. Оба могут перезаписать без предупреждения. Используйте -i для запроса перед перезаписью. Используйте -n, чтобы никогда не перезаписывать. mv атомарна в одной файловой системе — полезно для блокировок и атомарных обновлений.

bash
# cp: copy (original remains)
cp file.txt backup.txt
# mv: move/rename (original removed)
mv file.txt newname.txt
# Common mistake: overwriting
cp important.txt /tmp/  # If /tmp/important.txt exists, overwritten!
# Use -i for safety
cp -i file.txt /tmp/

Опасности rm

rm -rf опасен, особенно с переменными. Пустые переменные вызывают rm -rf /. Всегда заключайте в кавычки и проверяйте переменные. Рассмотрите trash-cli для восстанавливаемого удаления. Никогда не запускайте rm -rf / или с sudo неосторожно.

bash
# DANGEROUS: recursive force
rm -rf /  # Never do this!
rm -rf $VAR/  # If $VAR is empty, becomes rm -rf /
# Safer: check first
if [ -n "$VAR" ]; then
    rm -rf "$VAR/"
fi
# Use trash-cli instead
trash-put file.txt

Shebang

Shebang (#!) указывает интерпретатор. #!/usr/bin/env bash более переносим, чем #!/bin/bash. Используйте /bin/sh для максимальной переносимости (избегает bashisms). Всегда делайте скрипты исполняемыми через chmod +x.

bash
#!/bin/bash          # bash script
#!/usr/bin/env bash  # Portable bash
#!/usr/bin/env python3  # Python
#!/bin/sh            # POSIX sh (more portable)
# Make executable
chmod +x script.sh
./script.sh

Статус завершения

$? содержит статус завершения последней команды. 0 = успех, ненулевое = неудача. Проверяйте немедленно, пока другая команда не перезаписала его. Лучше использовать if command напрямую. Всегда завершайте скрипты со значимыми кодами статуса.

bash
# Check last command
if [ $? -eq 0 ]; then
    echo "Success"
fi
# Better: direct check
if command; then
    echo "Success"
else
    echo "Failed"
fi
# Exit with status
exit 0  # Success
exit 1  # Failure

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.