#!/usr/bin/env bash # # Copyright (c) 2025, Антон Аксенов # This file is part of m3u.su project # MIT License: https://git.axenov.dev/IPTV/web/src/branch/master/LICENSE # # shellcheck disable=SC2015 ######################################################## # Служебные исходные переменные ######################################################## # имя контейнера CONTAINER="iptv-web" # команда для запуска COMMAND="$1"; shift # имена всех новых, скопированных, изменённых и перемещённых php-файлов относительно корня репозитория FILES=($(git diff --name-only --diff-filter=ACMR HEAD 2>/dev/null | grep -e '.php$')) # признак запуска гитом (хук) IS_FROM_GIT="$(env | grep -c "GIT_EDITOR=:")" # признак запуска изнутри контейнера [[ -f /.dockerenv ]] && IS_FROM_CONTAINER=1 || IS_FROM_CONTAINER=0 # признак режима отладки [[ $LINTER_DEBUG -gt 1 ]] && set -x ######################################################## # Ввод/вывод ######################################################## which tput > /dev/null 2>&1 && [ "$(tput -T"$TERM" colors)" -gt 8 ] && CAN_USE_COLORS=1 || CAN_USE_COLORS=0 LINTER_COLORS=${LINTER_COLORS:-$CAN_USE_COLORS} [[ "$LINTER_COLORS" == 1 ]] && FRESET="$(tput sgr0)" || FRESET='' [[ "$LINTER_COLORS" == 1 ]] && FBOLD="$(tput bold)" || FBOLD='' [[ "$LINTER_COLORS" == 1 ]] && FDIM="$(tput dim)" || FDIM='' [[ "$LINTER_COLORS" == 1 ]] && FRED="$(tput setaf 1)" || FRED='' [[ "$LINTER_COLORS" == 1 ]] && FWHITE="$(tput setaf 7)" || FWHITE='' [[ "$LINTER_COLORS" == 1 ]] && FGREEN="$(tput setaf 2)" || FGREEN='' [[ "$LINTER_COLORS" == 1 ]] && FBRED="$(tput setab 1)" || FBRED='' [[ "$LINTER_COLORS" == 1 ]] && FBYELLOW="$(tput setab 3)" || FBYELLOW='' print() { echo -e "$*${FRESET}" } debug() { [[ "$LINTER_DEBUG" != 1 ]] && return if [ "$2" ]; then print "${FDIM}${FBOLD}${FRESET}${FDIM} ${FUNCNAME[1]:-?}():${BASH_LINENO:-?}\t$1 " >&2 else print "${FDIM}${FBOLD}${FRESET}${FDIM} $*" >&2 fi } var_dump() { debug "$1 = ${!1}" } title() { print "${FBOLD}${FWHITE}$*${FRESET}" } success() { print "${FBOLD}${FGREEN}$*${FRESET}" } warn() { print "${FBOLD}${FBYELLOW}${FRED} Внимание! ${FRESET} $*${FRESET}" } error() { print "${FBOLD}${FBRED}${FWHITE} Ошибка: ${FRESET}${FBOLD}${FRED} $*${FRESET}" >&2 } ######################################################## # Базовые хелперы ######################################################## # Проверяет существование функции с именем $1 is_function() { declare -F "$1" > /dev/null } # Выполняет команду в контейнере docker.exec() { if [[ "$IS_FROM_GIT" == 1 ]]; then cmd="docker exec -i $CONTAINER $*" else cmd="docker exec -it $CONTAINER $*" fi debug "Команда: $cmd" $cmd } ######################################################## # Хелперы для обработки команд ######################################################## # Выводит некоторую отладочную информацию status() { [[ "$LINTER_DEBUG" == 0 ]] && return debug "* Внутри контейнера: ${IS_FROM_CONTAINER}" debug "* Через git: ${IS_FROM_GIT}" debug "* Изменённых файлов: ${#FILES[@]}" } # Запускает phpcs в контейнере exec_phpcsniffer_pre() { docker.exec vendor/bin/phpcs -p "$@" } # Запускает phpcbf в контейнере exec_phpcsniffer_fix() { docker.exec vendor/bin/phpcbf "$@" } # Запускает php-cs-fixer в контейнере exec_phpcsfixer_pre() { docker.exec vendor/bin/php-cs-fixer fix -vv --config=./.php-cs-fixer.php --using-cache=no --format=@auto --diff --dry-run "$@" } # Запускает php-cs-fixer в контейнере exec_phpcsfixer_fix() { docker.exec vendor/bin/php-cs-fixer fix -vv --config=./.php-cs-fixer.php --using-cache=no --format=@auto "$@" } # Запускает phpstan в контейнере exec_phpstan() { docker.exec vendor/bin/phpstan -v --memory-limit=1G analyse "$@" } # Запускает phpunit в контейнере exec_phpunit() { docker.exec vendor/bin/phpunit "$@" } ######################################################## # Главные функции обработки команд ######################################################## # Устанавливает pre-commit git хук install() { status [[ -d ./.git/hooks ]] || { print "Не найден репозиторий '$(pwd)', пропускаю" exit } cp -f "$0" ./.git/hooks/pre-commit && \ success "Pre-commit hook установлен" || \ error "Pre-commit хук НЕ установлен" } # Удаляет pre-commit git хук remove() { status [[ -d ./.git/hooks ]] || { print "Не найден репозиторий '$(pwd)', пропускаю" exit } rm -f ./.git/hooks/pre-commit && \ success "Pre-commit hook удалён" || \ error "Pre-commit хук НЕ удалён" } # Запускает проверку код-стайла по всему проекту или только изменённым файлам style() { title "[php-cs-fixer] Запущена проверка код-стайла" status if [[ $# -gt 0 ]]; then exec_phpcsfixer_pre "$@" || { error "[php-cs-fixer] Проверка код-стайла завершена с ошибками!" exit 1 } else exec_phpcsfixer_pre "${FILES[@]}" || { error "[php-cs-fixer] Проверка код-стайла завершена с ошибками!" exit 1 } fi success "[php-cs-fixer] Проверка код-стайла завершена успешно!" } # Запускает исправление код-стайла по всему проекту или только изменённым файлам fix() { title "[php-cs-fixer] Запущено исправление код-стайла" status if [[ $# -gt 0 ]]; then exec_phpcsfixer_fix "$@" || { error "[php-cs-fixer] Исправление код-стайла завершено с ошибками!" exit 2 } else exec_phpcsfixer_fix "${FILES[@]}" || { error "[php-cs-fixer] Исправление код-стайла завершено с ошибками!" exit 2 } fi success "[php-cs-fixer] Исправление код-стайла завершено успешно!" } phpcs() { title "[phpcs] Запущена проверка код-стайла" status if [[ $# -gt 0 ]]; then exec_phpcsniffer_pre "$@" || { error "[phpcs] Проверка код-стайла завершена с ошибками!" exit 3 } else exec_phpcsniffer_pre "${FILES[@]}" || { error "[phpcs] Проверка код-стайла завершена с ошибками!" exit 3 } fi success "[phpcs] Проверка код-стайла завершена успешно!" } phpcbf() { title "[phpcs] Запущено исправление код-стайла" status if [[ $# -gt 0 ]]; then exec_phpcsniffer_fix "$@" || { error "[phpcs] Исправление код-стайла завершено с ошибками!" exit 4 } else exec_phpcsniffer_fix "${FILES[@]}"|| { error "[phpcs] Исправление код-стайла завершено с ошибками!" exit 4 } fi success "[phpcs] Исправление код-стайла завершено успешно!" } # Запускает статический анализ по всему проекту или только изменённым файлам stan() { title "[phpstan] Запуск статического анализа" status if [[ $# -gt 0 ]]; then exec_phpstan "$@" || { error "[phpstan] Статический анализ завершён с ошибками!" exit 5 } else exec_phpstan "${FILES[@]}" || { error "[phpstan] Статический анализ завершён с ошибками!" exit 5 } fi success "[phpstan] Статический анализ завершён успешно!" } # Запускает выполнение тестов tests() { title "[phpunit] Запуск тестирования" exec_phpunit "$@" || { error "[phpunit] Тестирование завершено с ошибками!" exit 6 } success "[phpunit] Тестирование завершено успешно!" } lint() { style phpcs stan } ######################################################## # Команды справки ######################################################## help() { print "${FBOLD}PHP-линтер" is_function "help.$1" && help."$1" && exit print "Использование:" print " ./linter КОМАНДА [АРГУМЕНТЫ]" print print "Доступные КОМАНДЫ:" print " h|help - вывести это сообщение" print " i|install - установить как pre-commit git хук" print " s|style - запустить проверку код-стайла (php-cs-fixer)" print " f|fix - запустить исправление код-стайла (php-cs-fixer)" print " p|phpcs - запустить проверку код-стайла (phpcs)" print " c|phpcbf - запустить исправление код-стайла (phpcbf)" print " a|stan - запустить статический анализ (phpstan)" print " l|lint - запустить style + phpcs + stan" print " t|tests - протестировать (phpunit)" print print "КОМАНДЫ 's', 'f', 'p', 'c' и 'a' могут принимать собственные АРГУМЕНТЫ php-cs-fixer, phpcs, phpcbf и phpstan соответственно." print print "Переменные окружения:" print " LINTER_COLORS - принудительно включить (1) или выключить (0, по умолчанию) форматированный вывод" print " LINTER_DEBUG - включить простую отладку (1), построчную (2) или выключить (0, по умолчанию)" } help.help() { print "КОМАНДА: help" print "Отображает справку по скрипту и его командам." print print "Использование:" print " ./linter help [КОМАНДА]" print print "Если КОМАНДА не указана, будет отображена справка по скрипту со списком допустимых КОМАНД." } help.install() { print "КОМАНДА: install" print "Устанавливает linter в качестве pre-commit git-хука." print print "Использование:" print " ./linter install" print print "Если в директории проекта нет репозитория, то установка будет пропущена без ошибки." print "Перед коммитом будут выполнены команды lint и tests." } help.style() { print "КОМАНДА: style" print "Запускает php-cs-fixer для проверки код-стайла." print "Поддерживает передачу собственных аргументов." print print "Использование:" print " ./linter style [АРГУМЕНТЫ]" print print "Допустимые АРГУМЕНТЫ: https://cs.symfony.com/doc/usage.html" print "Если АРГУМЕНТЫ не указаны, будут анализированы только изменённые и новые php-файлы." print "Если рабочее дерево git чистое, то инструмент отработает согласно своего конфига." } help.fix() { print "КОМАНДА: fix" print "Запускает php-cs-fixer для исправления код-стайла." print "Поддерживает передачу собственных аргументов." print print "Использование:" print " ./linter fix [АРГУМЕНТЫ]" print print "Допустимые АРГУМЕНТЫ: https://cs.symfony.com/doc/usage.html" print "Если АРГУМЕНТЫ не указаны, будут анализированы только изменённые и новые php-файлы." print "Если рабочее дерево git чистое, то инструмент отработает согласно своего конфига." } help.phpcs() { print "КОМАНДА: phpcs" print "Запускает phpcs для проверки код-стайла." print "Поддерживает передачу собственных аргументов." print print "Использование:" print " ./linter phpcs [АРГУМЕНТЫ]" print print "Допустимые АРГУМЕНТЫ: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage" print "Если АРГУМЕНТЫ не указаны, будут анализированы только изменённые и новые php-файлы." print "Если рабочее дерево git чистое, то инструмент отработает согласно своего конфига." } help.phpcbf() { print "КОМАНДА: phpcbf" print "Запускает phpcbf для исправления код-стайла." print "Поддерживает передачу собственных аргументов." print print "Использование:" print " ./linter phpcbf [АРГУМЕНТЫ]" print print "Допустимые АРГУМЕНТЫ: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Fixing-Errors-Automatically#using-the-php-code-beautifier-and-fixer" print "Если АРГУМЕНТЫ не указаны, будут анализированы только изменённые и новые php-файлы." print "Если рабочее дерево git чистое, то инструмент отработает согласно своего конфига." } help.stan() { print "КОМАНДА: stan" print "Запускает phpstan для статического анализа." print print "Использование:" print " ./linter stan [АРГУМЕНТЫ]" print print "Допустимые АРГУМЕНТЫ: https://phpstan.org/user-guide/command-line-usage" print "Если АРГУМЕНТЫ не указаны, будут анализированы только изменённые и новые php-файлы." print "Если рабочее дерево git чистое, то инструмент отработает согласно своего конфига." } help.lint() { print "КОМАНДА: lint" print "Последовательно запускает команды 'style', 'phpcs' и 'stan'." print "Не поддерживает передачу аргументов." print print "Использование:" print " ./linter lint" print print "Для дополнительной информации см. './linter help' для соответствующей команды." } help.tests() { print "КОМАНДА: tests" print "Последовательно запускает phpunit для статического анализа." print print "Использование:" print " ./linter tests [АРГУМЕНТЫ]" print print "Допустимые АРГУМЕНТЫ: https://docs.phpunit.de/en/10.5/textui.html#command-line-options" } ######################################################## # Точка входа ######################################################## if [[ "$IS_FROM_GIT" -gt 0 ]]; then status && lint && tests success "Коммит разрешён!" exit 0 fi case "$COMMAND" in h|help ) help "$1" ;; i|install ) install ;; r|remove ) remove ;; s|style ) style "$@" ;; f|fix ) fix "$@" ;; p|phpcs ) phpcs "$@" ;; c|phpcbf ) phpcbf "$@" ;; a|stan ) stan "$@" ;; l|lint ) lint ;; t|tests ) tests "$@" ;; * ) warn "неизвестная команда, вызываю help"; help ;; esac