명령줄 출력 기억/캐시

명령줄 출력 기억/캐시

일부 Python 및 C++ 프로그램을 순서대로 실행하는 bash 스크립트가 있습니다.

각 프로그램은 bash 스크립트에서 정의한 일부 입력 매개변수를 허용합니다. 예를 들어, 다음과 같이 프로그램을 실행합니다.

echo $param1 $param2 $param3 | python foo.py

Python 프로그램은 후속 프로그램에 대한 입력으로 사용하는 일부 값을 출력합니다. 위에서 말했듯이 어떤 파일에 값을 저장하고 거기에서 읽으면 파이썬 프로그램을 실행할 필요가 없습니다.

그래서 내 질문은 다음과 같습니다. 이 기능을 수행할 수 있는 일반적인 도구가 있습니까? 즉, 이렇게 실행할 수 있는 "bar"라는 프로그램이 있나요?

bar $param1 $param2 $param3 "python foo.py"

캐시 파일이 존재하는지 확인하고, 존재한다면 주어진 인수로 프로그램이 실행되었는지 확인하여, 그렇다면 프로그램을 다시 실행하는 대신 캐시된 출력 값을 출력합니다.

편집하다: 물론, 로그 파일 이름을 입력으로 제공할 수도 있습니다.

답변1

방금 이것에 대한 꽤 완전한 스크립트를 작성하는 데 괴짜가 생겼습니다.https://gist.github.com/akorn/51ee2fe7d36fa139723c851d87e56096.

sivann의 쉘 구현에 비해 장점:

  • 캐시 키를 계산할 때 환경 변수도 고려됩니다.
  • 경쟁 조건을 완전히 피하기 위해 무작위 대기에 의존하는 대신 잠금을 사용하십시오.
  • 더 적은 포크로 인해 긴밀한 루프에서 호출할 때 더 나은 성능
  • 또한 캐시 stderr
  • 완전히 투명함: 아무것도 인쇄되지 않습니다. 동일한 명령의 병렬 실행을 방해하지 않습니다. 캐싱에 문제가 있는 경우에만 캐시되지 않은 명령을 실행합니다.
  • 환경 변수 및 명령줄 스위치를 통해 구성 가능
  • 캐시를 정리할 수 있습니다(모든 오래된 항목 제거).

단점: bash가 아닌 zsh로 작성되었습니다.

#!/bin/zsh
#
# Purpose: run speficied command with specified arguments and cache result. If cache is fresh enough, don't run command again but return cached output.
# Also cache exit status and stderr.
# Copyright (c) 2019-2020 András Korn; License: GPLv3

# Use silly long variable names to avoid clashing with whatever the invoked program might use
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 "Usage: runcached [--ttl <max cache age>] [--cache-dir <cache directory>]"
    echo "       [--ignore-env] [--ignore-pwd] [--help] [--prune-cache]"
    echo "       [--] command [arg1 [arg2 ...]]"
    echo
    echo "Run 'command' with the specified args and cache stdout, stderr and exit"
    echo "status. If you run the same command again and the cache is fresh, cached"
    echo "data is returned and the command is not actually run."
    echo
    echo "Normally, all exported environment variables as well as the current working"
    echo "directory are included in the cache key. The --ignore options disable this."
    echo "The OLDPWD variable is always ignored."
    echo
    echo "--prune-cache deletes all cache entries older than the maximum age. There is"
    echo "no other mechanism to prevent the cache growing without bounds."
    echo
    echo "The default cache directory is ${RUNCACHED_CACHE_DIR}."
    echo "Maximum cache age defaults to ${RUNCACHED_MAX_AGE}."
    echo
    echo "CAVEATS:"
    echo
    echo "Side effects of 'command' are obviously not cached."
    echo
    echo "There is no cache invalidation logic except cache age (specified in seconds)."
    echo
    echo "If the cache can't be created, the command is run uncached."
    echo
    echo "This script is always silent; any output comes from the invoked command. You"
    echo "may thus not notice errors creating the cache and such."
    echo
    echo "stdout and stderr streams are saved separately. When both are written to a"
    echo "terminal from cache, they will almost certainly be interleaved differently"
    echo "than originally. Ordering of messages within the two streams is preserved."
    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

# the built-in mv doesn't fall back to copy if renaming fails due to EXDEV;
# since the cache directory is likely on a different fs than the tmp
# directory, this is an important limitation, so we use /bin/mv instead
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 # if no command specified, exit silently

(
    # Almost(?) nothing uses OLDPWD, but taking it into account potentially reduces cache efficency.
    # Thus, we ignore it for the purpose of coming up with a cache key.
    unset OLDPWD
    ((RUNCACHED_IGNORE_PWD)) && unset PWD
    ((RUNCACHED_IGNORE_ENV)) || env
    echo -E "$@"
) | md5sum | read RUNCACHED_CACHE_KEY RUNCACHED__crap__
# make the cache dir hashed unless a cache file already exists (created by a previous version that didn't use hashed dirs)
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

# If we can't obtain a lock, we want to run uncached; otherwise
# 'runcached' wouldn't be transparent because it would prevent
# parallel execution of several instances of the same command.
# Locking is necessary to avoid races between the mv(1) command
# below replacing stderr with a newer version and another instance
# of runcached using a newer stdout with the older 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

    # only reached if cache didn't exist or was too old
    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

# only reached if cache not created successfully or lock couldn't be obtained
exec $@

답변2

구현은 다음과 같습니다.https://bitbucket.org/sivann/runcached/src실행 파일 경로, 출력, 종료 코드를 캐시하고 매개변수를 기억합니다. 구성 가능한 만료 시간. Bash, C 또는 Python으로 구현하고 자신에게 적합한 것을 선택하세요. "bash" 버전에는 몇 가지 제한 사항이 있습니다.

답변3

내가 만든 Bash 사용자의 경우배쉬 캐시, András Korn의 zsh 접근 방식과 유사한 기능 세트를 제공합니다. 환경 변수를 지원하고, 경합을 방지하고, stderr 및 종료 코드를 캐시하고, 오래된 데이터를 무효화하고, 비동기 워밍업 및 뮤텍스를 지원하는 등의 작업을 수행합니다.

예를 들어 다음과 같이 설명하는 내용을 구현할 수 있다고 생각합니다.

foo() {
  python foo.py <<<"$*" # this writes all arguments to the python stdin
} && bc::cache foo

그런 다음 다음과 같이 호출할 수 있습니다.

foo param1 param2 param3

foo결과를 캐시하고 후속 호출에서 재사용합니다 .

날 봐이전 답변자세한 내용은.

답변4

기존 패키지를 기반으로 한 간단한 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

관련 정보