Привет, Хабр! На связи команда «Гравитон». В этой небольшой статье мы собрали подборку практических приемов для эффективной работы с bash, которые помогут упростить интерактивное взаимодействие, а также повысить надежность и читаемость скриптов.

History expansion 

Многие пользователи знакомы с базовым использованием history expansion, например, повторением последней команды через !!. Однако этот механизм предоставляет гораздо более широкие возможности.

!!:0 — повторное выполнение предыдущей команды (имя команды без аргументов)

!!:-n — повторное выполнение n-й предыдущей команды

!!:n — получение n-го аргумента из истории (нумерация с 1)

!!:p — модификатор предпросмотра (показывает команду без выполнения)

!!:n* - получение аргументов с n-го до конца

shell
[root@localhost ~]# cat /proc/cpuinfo | head -n 3
hart	: 0
isa 	: rv64acdfimsu
mmu 	: sv48

[root@localhost ~]# !!:0 !:1:s/cpuinfo/meminfo/ !:2*

cat /proc/meminfo | head -n 3
MemTotal:    	186324 kB
MemFree:     	177164 kB
MemAvailable:	176780 kB

Альтернативный синтаксис для быстрого редактирования:

shell
[root@localhost ~]# cat /proc/cpuinfo | head -n 3
hart : 0
isa : rv64acdfimsu
mmu : sv48

[root@localhost ~]# ^cpuinfo^meminfo
cat /proc/meminfo | head -n 3
MemTotal: 186324 kB
MemFree: 177164 kB
MemAvailable: 176748 kB

Быстрый способ чтения файла

Вместо чтения файла в переменную вот так:

shell
cfg="cat cfg.ini"
echo $cfg

существует более элегантный способ с помощью process substitution:

shell
cfg=$(<config.ini)

Кстати, построчно сохранить этот файл в массив возможно с помощью команды mapfile:

shell
mapfile -t lines < <(grep -v '^#' config.ini)
printf '%s\n' "${lines[@]}"

Pipefail: валидация данных в конвейерах

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

Это соответствует принципу идиоматичного bash-программирования — явное лучше неявного.

Например:

shell
#!/bin/bash
# Парсим конфигурационный файл
grep "timeout" /etc/app/config.conf | cut -d'=' -f2 | tr -d ' '
echo "Парсинг завершен с кодом: $?"
# Если параметр timeout отсутствует, grep вернет 1, но пайплайн "успешен"
# Мы получим пустой вывод без индикации ошибки

Правильная реализация:

shell
#!/bin/bash
set -o pipefail  # Требуем успешного выполнения всех этапов
# Валидация наличия обязательного параметра
grep "timeout" /etc/app/config.conf | cut -d'=' -f2 | tr -d ' '
# При отсутствии параметра timeout скрипт получит код 1 от grep
# что корректно отразит семантическую ошибку конфигурации

Pipefail превращает пайплайн в средство валидации — он гарантирует, что не только синтаксис команд корректен, но и семантика данных соответствует ожиданиям.

Параллельное выполнение функций

shell
#!/usr/bin/env bash
process_data() {
	local input="$1"

	echo "Обрабатываем '$input' в PID: $$"

	sleep 1  # Имитация тяжелой операции

	echo "Результат: $(echo "$input" | md5sum)"
}
#Попытка параллельного выполнения не работает
printf "%s\n" {1..5} | xargs -P5 -I{} bash -c 'process_data "{}"'
bash: line 1: process_data: command not found

 Для решения этой проблемы достаточно экспортировать функцию, как показано в примере:

shell
#!/usr/bin/env bash
process_data() {
	local input="$1"

	echo "[PID:$$] Обрабатываем: $input"

	sleep 1

	echo "[PID:$$] Результат: $(( input * 2 ))"
}
# Ключевая строка: делаем функцию доступной в подпроцессах
export -f process_data
# Теперь работает
printf "%s\n" {1..5} | xargs -P5 -I{} bash -c 'process_data "{}"'
[PID:4567] Обрабатываем: 1
[PID:4568] Обрабатываем: 2 
[PID:4569] Обрабатываем: 3
[PID:4570] Обрабатываем: 4
[PID:4571] Обрабатываем: 5
[PID:4567] Результат: 2
[PID:4568] Результат: 4
[PID:4569] Результат: 6
[PID:4570] Результат: 8
[PID:4571] Результат: 10

Эмуляция наследования

shell
#!/usr/bin/env bash
set -euo pipefail
# Диспетчер базовых операций
super() {
	local -r fn="${FUNCNAME[1]}"  # Получаем имя вызываемой функции

	local -a parts=( ${fn//::/ } )  # Разбираем имя функции на компоненты

	"${parts[0]}::base::${parts[2]}" "$@" # Вызываем базовую реализацию
}

Базовая операция бэкапа

deploy::base::backup() {
	local env="$1"

	local app="$2"

	echo "Создаем бэкап $app в окружении $env"

	tar -czf "/backups/${env}-${app}-$(date +%Y%m%d).tar.gz" "/var/www/$app"
}

Бэкап для тестового окружения

deploy::test::backup() {
	local app="$1"

	# В тестовом окружении бэкапы храним меньше времени

	super "test" "$app"

	find /backups -name "test-*" -mtime +7 -delete

	echo "Удалены старые тестовые бэкапы (старше 7 дней)"
}

Бэкап для продакшена 

deploy::prod::backup() {
	local app="$1"

	# В проде делаем полный бэкап БД + файлы

	super "production" "$app"

	echo "Создаем бэкап базы данных"

	pg_dump myapp > "/backups/prod-${app}-db-$(date +%Y%m%d).sql"
}

Использование

echo "=== Бэкап на разных окружениях ==="
echo "--- Тестовое окружение ---"
deploy::test::backup "myapp"
echo ""
echo "--- Продакшен окружение ---"
deploy::prod::backup "myapp"

Этот паттерн привносит в bash принципы ООП, сохраняя при этом простоту и прозрачность shell-скриптинга. Он особенно полезен в DevOps-инструментах и системах сборки, где требуется модульность и гибкость.

Заключение

Надеюсь, приведенные примеры показались вам интересными и полезными. Благодарим за ваше время и внимание. Эффективных скриптов и элегантных решений вам!