← Заметки

Чтение YAML из shell: проверка порта и запуск команды без eval

На этой странице

Чтобы прочитать порт YAML из shell без eval, используйте YAML-парсер, проверьте тип и диапазон значения и передавайте только нужные переменные. Пример читает одно значение server.port через Python и PyYAML и запускает дочернюю команду только после успешной проверки. Не приближайте полную грамматику YAML через sed/awk.

Проверка только отображённого текста не позволяет надёжно отличить YAML-целое 8080 от строки "8080". Загрузчик не преобразует строки в числа: принимает лишь значение, разобранное как целое от 1 до 65535. bool в Python — подкласс int, поэтому type(port) is int отклоняет булевы значения, а isinstance(port, int) не отклонил бы.

Порядок обработки

Разберите и проверьте целочисленный порт до экспорта SERVER_PORT. Ошибка загрузчика не даёт запустить целевую команду; exec сохраняет её статус.

Разберите и проверьте целочисленный порт до экспорта SERVER_PORT. Ошибка загрузчика не даёт запустить целевую команду; exec сохраняет её статус.

Среда и входные данные

Используйте Python 3 и PyYAML 6.0.3. Локальная среда выполнения использует Python 3.13. Сохраните три файла в одном каталоге и установите зависимость в отдельное виртуальное окружение.

python3 -m venv .venv
.venv/bin/python -m pip install PyYAML==6.0.3

application.yml

server:
  port: 8080

read-port.py

import sys
from pathlib import Path
import yaml

if len(sys.argv) != 2:
    sys.exit("usage: read-port.py CONFIG")
try:
    with Path(sys.argv[1]).open(encoding="utf-8") as stream:
        config = yaml.safe_load(stream)
    if not isinstance(config, dict):
        raise ValueError("root must be a mapping")
    server = config.get("server")
    if not isinstance(server, dict):
        raise ValueError("server must be a mapping")
    port = server.get("port")
    if type(port) is not int or not 1 <= port <= 65535:
        raise ValueError("server.port must be an integer from 1 to 65535")
except (OSError, UnicodeError, yaml.YAMLError, ValueError) as error:
    print(f"invalid configuration: {error}", file=sys.stderr)
    sys.exit(2)
print(port)

run-with-port.sh

#!/bin/sh
set -eu
if [ "$#" -lt 2 ]; then
    printf '%s\n' 'usage: run-with-port.sh CONFIG COMMAND [ARG ...]' >&2
    exit 2
fi
config=$1
shift
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
port=$("${PYTHON:-python3}" "$script_dir/read-port.py" "$config")
export SERVER_PORT="$port"
exec "$@"

Запуск команды с проверенным значением

PYTHON=.venv/bin/python sh run-with-port.sh application.yml \
  .venv/bin/python -c 'import os; print(os.environ["SERVER_PORT"])'
8080

read-port.py пишет только допустимое значение в stdout, ошибки — в stderr. Отсутствующие файлы, неверный YAML, несколько документов, ввод не в виде отображения, отсутствующие поля, строки, булевы и дробные значения, выход за диапазон отклоняются со статусом 2. run-with-port.sh передаёт ошибку, не запуская целевую команду. После запуска exec сохраняет её код завершения.

"$@" сохраняет команду и каждый аргумент отдельно. Пробелы или $() в аргументе не вычисляются повторно как shell-код. Относительные пути PYTHON и конфигурации разрешаются от рабочего каталога вызывающего процесса, а обёртка находит read-port.py рядом с собой.

Экспорт достигает дочернего процесса, не меняя окружение вызывающей родительской оболочки. Запускайте обёртку через sh run-with-port.sh ..., а не через source. Отдельно проверьте, действительно ли приложение читает SERVER_PORT.

Границы загрузчика

safe_load не разрешает теги, создающие произвольные объекты Python, но прикладные проверки типов и диапазонов всё равно нужны. Пример не ограничивает память и время для злонамеренно больших данных, поэтому предназначен для доверенной локальной конфигурации.

Он следует неявным правилам типов PyYAML: значения вроде on могут стать булевыми. Повторяющиеся ключи не отклоняются, позднее значение может сохраниться; запрещайте их во входных данных и проверяйте отдельно. Это не полная политика типов YAML 1.2 и не универсальный валидатор схем конфигурации. Для каждого нового поля явно задавайте тип и поведение при отсутствии значения.

Об ошибках команд и кодах завершения см. запрос выпусков GitHub. О совместимости сохраняемых данных заданий JVM см. миграцию Spring Batch 5.

Обсуждение

Посмотреть на GitHub ↗