Вопрос или проблема
У меня есть bash-скрипт, который я использую для запуска нескольких программ на Python и C++ в последовательности.
Каждая программа принимает некоторые входные параметры, которые я определяю в bash-скрипте. Например, я запускаю программу так:
echo $param1 $param2 $param3 | python foo.py
Программа на Python выводит некоторые значения, которые мы используем в качестве входных данных для последующих программ. Как я уже говорил выше, мне не нужно запускать программу на Python, если я сохраняю значения в файл и читаю их оттуда.
Так что мой вопрос: существует ли какой-то универсальный инструмент, который реализует эту функцию? То есть, есть ли какая-то программа под названием ‘bar’, которую я мог бы запустить так:
bar $param1 $param2 $param3 "python foo.py"
которая проверяла бы, существует ли файл кэша, если да, то проверяла бы, была ли программа запущена с данными параметрами, и если да, выводила бы кэшированные выходные значения вместо повторного запуска программы.
Правка: Имя файла журнала также может быть передано в качестве входного параметра, конечно.
Я только что увлёкся созданием довольно полного скрипта для этого; последняя версия доступна по адресу https://gist.github.com/akorn/51ee2fe7d36fa139723c851d87e56096.
Преимущества по сравнению с реализацией оболочки sivann:
- также учитывает переменные окружения при вычислении ключа кэша;
- совершенно избегает условий гонки, используя блокировки вместо полагания на случайные ожидания;
- лучшие показатели производительности при вызове в узком цикле из-за меньшего количества форков;
- также кэширует stderr;
- совершенно прозрачно: ничего не печатает; не мешает параллельному выполнению одной и той же команды; просто выполняет команду без кэша, если есть проблемы с кэшем;
- настраивается через переменные окружения и параметры командной строки;
- может очищать кэш (удалять все устаревшие записи);
Недостаток: написан на zsh, а не на bash.
#!/bin/zsh
#
# Цель: выполнить указанную команду с указанными аргументами и кэшировать результат. Если кэш свежий, не выполнять команду снова, а вернуть кэшированный вывод.
# Также кэшируется код завершения и stderr.
# Авторские права (c) 2019-2020 Андраш Корн; Лицензия: GPLv3
# Используйте глупо длинные имена переменных, чтобы избежать конфликтов с тем, что может использовать вызываемая программа
RUNCACHED_MAX_AGE=${RUNCACHED_MAX_AGE:-300}
RUNCACHED_IGNORE_ENV=${RUNCACHED_IGNORE_ENV:-0}
RUNCACHED_IGNORE_PWD=${RUNCACHED_IGNORE_PWD:-0}
[[ -n "$HOME" ]] && RUNCACHED_CACHE_DIR=${RUNCACHED_CACHE_DIR:-$HOME/.runcached}
RUNCACHED_CACHE_DIR=${RUNCACHED_CACHE_DIR:-/var/cache/runcached}
function usage() {
echo "Использование: runcached [--ttl <максимальный возраст кэша>] [--cache-dir <каталог кэша>]"
echo " [--ignore-env] [--ignore-pwd] [--help] [--prune-cache]"
echo " [--] command [arg1 [arg2 ...]]"
echo
echo "Выполнить 'command' с указанными аргументами и кэшировать stdout, stderr и завершение"
echo "статус. Если вы снова запускаете ту же команду и кэш свежий, возвращаются"
echo "данные из кэша, и команда на самом деле не запускается."
echo
echo "Обычно все экспортируемые переменные окружения, а также текущий рабочий"
echo "каталог включены в ключ кэша. Опции --ignore отключают это."
echo "Переменная OLDPWD всегда игнорируется."
echo
echo "--prune-cache удаляет все записи кэша, старше максимального возраста. Нет"
echo "другого механизма, чтобы предотвратить неограниченный рост кэша."
echo
echo "Каталог кэша по умолчанию ${RUNCACHED_CACHE_DIR}."
echo "Максимальный возраст кэша по умолчанию ${RUNCACHED_MAX_AGE}."
echo
echo "ОГРАНИЧЕНИЯ:"
echo
echo "Побочные эффекты 'command' явно не кэшируются."
echo
echo "Нет логики недействительности кэша, кроме возраста кэша (указанного в секундах)."
echo
echo "Если кэш не может быть создан, команда выполняется без кэша."
echo
echo "Этот скрипт всегда молчит; любой вывод поступает от вызываемой команды. Вы"
echo "можете таким образом не заметить ошибки при создании кэша и тому подобное."
echo
echo "Потоки stdout и stderr сохраняются отдельно. Когда оба записываются в"
echo "терминал из кэша, они, вероятно, будут перемешаны иначе"
echo "чем оригинально. Порядок сообщений в двух потоках сохраняется."
exit 0
}
while [[ -n "$1" ]]; do
case "$1" in
--ttl) RUNCACHED_MAX_AGE="$2"; shift 2;;
--cache-dir) RUNCACHED_CACHE_DIR="$2"; shift 2;;
--ignore-env) RUNCACHED_IGNORE_ENV=1; shift;;
--ignore-pwd) RUNCACHED_IGNORE_PWD=1; shift;;
--prune-cache) RUNCACHED_PRUNE=1; shift;;
--help) usage;;
--) shift; break;;
*) break;;
esac
done
zmodload zsh/datetime
zmodload zsh/stat
zmodload zsh/system
zmodload zsh/files
# встроенный mv не переходит к копированию, если переименование не удалось из-за EXDEV;
# так как каталог кэша, вероятно, находится на другой файловой системе, чем временный
# каталог, это важное ограничение, поэтому мы используем /bin/mv вместо
disable mv
mkdir -p "$RUNCACHED_CACHE_DIR" >/dev/null 2>/dev/null
((RUNCACHED_PRUNE)) && find "$RUNCACHED_CACHE_DIR/." -maxdepth 1 -type f \! -newermt @$[EPOCHSECONDS-RUNCACHED_MAX_AGE] -delete 2>/dev/null
[[ -n "$@" ]] || exit 0 # если команда не указана, выйти тихо
(
# Почти(?) ничего не использует OLDPWD, но учитывая это, потенциально снижается эффективность кэша.
# Таким образом, мы игнорируем его для цели создания ключа кэша.
unset OLDPWD
((RUNCACHED_IGNORE_PWD)) && unset PWD
((RUNCACHED_IGNORE_ENV)) || env
echo -E "$@"
) | md5sum | read RUNCACHED_CACHE_KEY RUNCACHED__crap__
# сделать каталог кэша хэшированным, если файл кэша еще не существует (созданный предыдущей версией, которая не использовала хэшированные каталоги)
if ! [[ -f $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.exitstatus ]]; then
RUNCACHED_CACHE_KEY=$RUNCACHED_CACHE_KEY[1,2]/$RUNCACHED_CACHE_KEY[3,4]/$RUNCACHED_CACHE_KEY[5,$]
mkdir -p "$RUNCACHED_CACHE_DIR/${RUNCACHED_CACHE_KEY:h}" >/dev/null 2>/dev/null
fi
# Если мы не можем получить блокировку, мы хотим выполнить без кэша; в противном случае
# 'runcached' не был бы прозрачным, потому что это помешало бы
# параллельному выполнению нескольких экземпляров одной и той же команды.
# Блокировка необходима, чтобы избежать гонок между командой mv(1)
# ниже, заменяющей stderr новой версией и другим экземпляром
# runcached, использующим новый stdout с более старым stderr.
: >>$RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.lock 2>/dev/null
if zsystem flock -t 0 $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.lock 2>/dev/null; then
if [[ -f $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.stdout ]]; then
if [[ $[EPOCHSECONDS-$(zstat +mtime $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.stdout)] -le $RUNCACHED_MAX_AGE ]]; then
cat $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.stdout &
cat $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.stderr >&2 &
wait
exit $(<$RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.exitstatus)
else
rm -f $RUNCACHED_CACHE_DIR/$RUNCACHED_CACHE_KEY.{stdout,stderr,exitstatus} 2>/dev/null
fi
fi
# достижимо только если кэш не существовал или был слишком старым
if [[ -d $RUNCACHED_CACHE_DIR/. ]]; then
RUNCACHED_tempdir=$(mktemp -d 2>/dev/null)
if [[ -d $RUNCACHED_tempdir/. ]]; then
$@ >&1 >$RUNCACHED_tempdir/${RUNCACHED_CACHE_KEY:t}.stdout 2>&2 2>$RUNCACHED_tempdir/${RUNCACHED_CACHE_KEY:t}.stderr
RUNCACHED_ret=$?
echo $RUNCACHED_ret >$RUNCACHED_tempdir/${RUNCACHED_CACHE_KEY:t}.exitstatus 2>/dev/null
mv $RUNCACHED_tempdir/${RUNCACHED_CACHE_KEY:t}.{stdout,stderr,exitstatus} $RUNCACHED_CACHE_DIR/${RUNCACHED_CACHE_KEY:h} 2>/dev/null
rmdir $RUNCACHED_tempdir 2>/dev/null
exit $RUNCACHED_ret
fi
fi
fi
# достижимо только если кэш не был успешно создан или блокировку не удалось получить
exec $@
Реализация существует здесь: https://github.com/sivann/runcached. Кэширует исполняемый путь, вывод, код завершения, запоминает аргументы. Настраиваемое время истечения. Реализовано на bash, C, python, выбирайте, что вам подходит. Версия “bash” несколько ограничена.
Для пользователей Bash я создал bash-cache, который предоставляет аналогичный набор функций подходу Андраша Корна на zsh. Он поддерживает переменные окружения, избегает гонок, кэширует stderr и коды завершения, недействительные устаревшие данные, поддерживает асинхронное разогревание и мьютекс-блокировки и многое другое.
Например, я думаю, что вы можете реализовать то, что описали, так:
foo() {
python foo.py <<<"$*" # это записывает все аргументы в stdin python
} && bc::cache foo
Которую вы затем вызовете как:
foo param1 param2 param3
И foo
будет кэшировать результаты и повторно использовать их в последующих вызовах.
Смотрите мою предыдущую ответ для получения дополнительных сведений.
Простое решение на Python, основанное на существующих пакетах (joblib.Memory для мемоизации):
cmdcache.py:
import sys, os
import joblib
import subprocess
mem = joblib.Memory('.', verbose=False)
@mem.cache
def run_cmd(args, env):
process = subprocess.run(args, capture_output=True, text=True)
return process.stdout, process.stderr, process.returncode
stdout, stderr, returncode = run_cmd(sys.argv[1:], dict(os.environ))
sys.stdout.write(stdout)
sys.stdout.write(stderr)
sys.exit(returncode)
Пример использования:
python cmdcache.py ls
Ответ или решение
Мемоизация и кэширование вывода командной строки
Введение
Кэширование вывода программ, особенно тех, которые вызываются из Bash-скриптов, может значительно улучшить производительность процессов, упростить автоматизацию задач и сэкономить ресурсы. В ситуации, когда множество программ взаимодействуют друг с другом, кэширование результата одной программы как входа для другой программы позволяет избежать повторного выполнения затратных вычислений.
Зачем это нужно?
Как правило, некоторые команды выполняют длительные вычисления или обращения к внешним ресурсам. Если вывод этих команд может быть повторно использован, то нет необходимости каждый раз запускать их заново. Кэширование позволяет:
- Сэкономить время: избегаете повторных вызовов тех же команд.
- Снизить нагрузку на ресурсы: уменьшаете количество параллельных процессов.
- Упростить логику обработки: результаты сохраняются в файлы, к которым можно быстро обратиться.
Как это реализовать?
Для решения задачи кэширования вывода команды при запуске из Bash-скриптов можно воспользоваться несколькими подходами:
-
Скрипты на языке shell:
Вы можете создать Bash-скрипт, который будет принимать параметры и выполнять команду только в случае, если результаты уже не кэшированы или устарели. Ниже приведён пример простого Bash-скрипта:#!/bin/bash CACHE_FILE="cache_output.txt" CACHE_TTL=300 # Время жизни кеша в секундах if [[ -f $CACHE_FILE && $(($(date +%s) - $(stat -c %Y $CACHE_FILE))) -le $CACHE_TTL ]]; then cat $CACHE_FILE else echo "Running command..." OUTPUT=$(echo "$1 $2 $3" | python foo.py) # вызов Python скрипта echo "$OUTPUT" > $CACHE_FILE echo "$OUTPUT" # вывод результата fi
Этот скрипт сначала проверяет, существует ли кэш и актуален ли он. Если да, то просто выводит содержимое кэша, иначе запускает команду и сохраняет результат.
-
Использование сторонних инструментов:
Существуют уже готовые решения, которые выполняют кэширование. Например, популярные инструменты, которые упоминались в вашем вопросе:- runcached: Этот инструмент на Zsh обеспечивает кэширование вывода команд, учитывает переменные окружения и текущую директорию.
- bash-cache: Реализация аналогичных функций в Bash с поддержкой кэширования stderr и кода завершения.
Пример команды с использованием
bash-cache
будет следующий:foo() { python foo.py "$@" } && bc::cache foo
Вызывая
foo param1 param2 param3
, скрипт будет кэшировать результаты. -
Python решения:
Если вам удобнее работать на Python, вы можете использовать библиотекуjoblib
, чтобы кэшировать вывод команд:import sys import joblib import subprocess mem = joblib.Memory('.', verbose=0) @mem.cache def run_cmd(args): return subprocess.run(args, capture_output=True, text=True) stdout, stderr, returncode = run_cmd(sys.argv[1:]) sys.stdout.write(stdout) sys.stderr.write(stderr) sys.exit(returncode)
Этот код кэширует вызовы команд, что позволяет повторно использовать результаты выполнения.
Заключение
Кэширование вывода командных строк — важная практика для повышения эффективности работы с Bash-скриптами. Используя предложенные инструменты или создавая свои собственные решения, вы можете значительно оптимизировать производительность ваших скриптов и уменьшить нагрузку на ресурсы. Выбор между реализации на Bash, Zsh или Python зависит от ваших предпочтений и условий выполнения задач.