Привет, Хабр! На связи команда «Гравитон». В этой небольшой статье мы собрали подборку практических приемов для эффективной работы с 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-инструментах и системах сборки, где требуется модульность и гибкость.
Заключение
Надеюсь, приведенные примеры показались вам интересными и полезными. Благодарим за ваше время и внимание. Эффективных скриптов и элегантных решений вам!
