Перейти к основному содержимому

Bash скрипты

· 5 мин. чтения
Дмитрий Чудный
Автор сайта

Используем плагины и инструменты

🔥 Обязательные требования

Указываем командную оболочку для выполнения скриптов: Bash

#!/usr/bin/env bash

Разные оболочки по-разному выполняют команды.

Режим прекращения выполнения в случае ошибки любой команды

set -eo pipefail

Используем $SELF_DIR для относительных путей

SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
RELATIVE_PATH=${RELATIVE_PATH:-"$SELF_DIR/relative/path"}

Задаём переменным значения по-умолчанию

CI_BUILD_NUMBER=${CI_BUILD_NUMBER:-0}

Даём возможность задавать значения параметров снаружи

# ❌ Плохо: нельзя передать другое значение снаружи
XCODE_APP_NAME="application"
# ✅ Хорошо: можно задать любое значение переменной снаружи
XCODE_APP_NAME=${XCODE_APP_NAME:-"application"}

Делим скрипт на логические части при помощи функций

task_foo () {
# Script of task foo
}

task_boo () {
# Script of task boo
}

task_foo
# task_boo - временно отключено

🟢 Хорошие практики и шаблоны

Ветвление по имени команды в первом аргументе

task_foo () {
echo "exec: task_foo"
}

task_boo () {
echo "exec: task_boo first arg: $1"
echo "exec: task_boo all task args: $\*"
}

"$@"
./script.sh task_boo --one --two
exec: task_boo first arg: --one
exec: task_boo all task args: --one --two

Скрипт с подкомандами

#!/usr/bin/env bash
set -eo pipefail
SELF_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"

clean() {
echo "[INFO] clean"
}

build() {
echo "[INFO] build"
}

deploy() {
echo "[INFO] deploy"
}

default() {
clean
build
deploy
}

if [[ -z "$1" ]]; then
default
elif [[ $(type -t "$1") == function ]]; then
"$@"
else
echo "Unknown script command: $1" && exit 1
fi

✅ Более короткий вариант проверки вызова только функции из скрипта

[[ ! $(type -t "$1") == function ]] && echo "Not specified or invalid script command: $1" && exit 1

Определение путей относительно исполняемого скрипта

BASH_SOURCE[0] содержит путь, к скрипту, который исполняется.

Он может быть как абсолютным, так и относительным.

В тексте скрипта надёжнее использовать абсолютные пути, которые не зависят от текущей папки.

Чтобы получить абсолютные пути относительно расположения исполняемого скрипта, используем команды перечисленные далее.

SELF_DIR

# Папка расположения исполняемого скрипта
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"

SELF_PATH

# Полный путь расположения исполняемого скрипта
SELF_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"

Логирование в начала и конца выполнения скрипта

SELF_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
echo "($SELF_PATH) begin ..."
# код скрипта
echo "($SELF_PATH) done"

Выполнение поиска бинарника для выполнения вместо явного указания пути

Допустим не известно точно, где расположен бинарник deno, но зато оболочка знает как его найти, тогда используем:

#!/usr/bin/env -S deno run

Параметр -S указываем обязательно, чтобы строка команды с аргументами была разделена через пробел.

Без -S она передаётся как один аргумент и как правило происходит ошибка.

См. 23.2.2 -S/--split-string usage in scripts

Чтение значения параметра из .env фала

Значение может быть без кавычек или в двойных кавычках

CI_BUILD_VERSION_PREFIX=$(sed -nE 's/^CI_BUILD_VERSION_PREFIX\="?([^"]*)"?$/\1/p' "$SELF_DIR/common.env")
CI_BUILD_VERSION_PREFIX=1.0.0
# или
CI_BUILD_VERSION_PREFIX="1.0.0"

Скрипт вызван на выполнение или подключен через source

#!/bin/bash
# Определить какое-то значение
get_some_value() {
echo "Это значение, которое надо было определить"
}

if [ "$0" = "${BASH_SOURCE[0]}" ]; then
# ✅ Тут функция будет вызвана, только если скрипт запушен на выполнение
# ⭕️ Не будет вызвана, если скрипт подключен через \`source\`
get_some_value
fi
# ✅ Получение и отображение в логе значения, вызовом скрипта
./get_some_value.sh

# ✅ Получение значение, вызовом функции из скрипта
source ./get_some_value.sh

if [[ "\<условие>" ]]; then
some_value
fi

Короткий if/else для определения значения (аналог тернарного if ? then : else)

local exclude_platform
[[ $PLATFORM = "ios" ]] && exclude_platform="android" || exclude_platform="ios"

Прочитать значение поля из JSON файла

local version
version=$(node -p "require('./package.json').version")local version
version=$(deno eval -p "(await import('./package.json', {assert: {type: 'json'}})).default.version")

Проверить есть ли скрипт в package.json scripts

local mycustomscript
mycustomscript=$(node -p "require('./package.json').scripts.mycustomscript || ''")
if [[ -n "$mycustomscript" ]]; then
npm run mycustomscript
fi

Задать значение переменной окружения по-умолчанию в зависимости от значения другой переменной окружения

export RUBY_SETUP=${RUBY_SETUP:-$([ "$PLATFORM" = "ios" ] && echo "true")}
export GEMS_INSTALL=${GEMS_INSTALL:-$([ "$PLATFORM" = "ios" ] && echo "true")}

Получить имя файла из пути к файлу

file_name="$(basename "$file_path")"

Получить расширение файла или имя файла без расширения

# file_name: development.android.env

extension_min="${file_name##\*.}"
# extension_min: env

extension_max="${file_name#\*.}"
# extension_max: android.env

filename_min="${file_name%%.\*}"
# filename_min: development

filename_max="${file_name%.\*}"
# filename_max: development.android

Найти исполняемые или не исполняемые файлы

# Найти исполняемые файлы
find .git/hooks -type f -name 'pre-\*' -maxdepth 1 -not -perm -a=x -print

# Найти НЕ исполняемые файлы
find .git/hooks -type f -name 'pre-\*' -maxdepth 1 -perm -a=x -print

Добавить числовое значение к номеру сборки в переменной окружения

export CI_BUILD_NUMBER=${CI_BUILD_NUMBER:-0}
export BUILD_NUMBER_SHIFT=${BUILD_NUMBER_SHIFT:-0}
export BUILD_NUMBER=$((CI_BUILD_NUMBER + BUILD_NUMBER_SHIFT))

Получить текущие дату, время и зону

# 2023-09-01 14:49:29 MSK
date +'%Y-%m-%d %H:%M:%S %Z'

Сравнить файлы по содержимому

if cmp -s "path/to/file1" "path/to/file2"; then
echo "Files equals"
fi

Удалить содержимое директории без удаления самой директории

shopt -s dotglob
rm -fr ./path/to/dir/*

Подгрузить переменные окружения из .env файла, сделав им export

set -o allexport;
source .env
set +o allexport;

Прочитать версию bundler из Gemfile.lock

BUNDLED WITH
2.1.4

Команда для получения чистого значения версии

grep -A 1 "BUNDLED WITH" Gemfile.lock | tail -n 1 | xargs

Ссылки