명령줄 프로그램을 메모(캐싱)하시겠습니까?

명령줄 프로그램을 메모(캐싱)하시겠습니까?

때로는 동일한 출력을 얻기 위해 동일하고 비용이 많이 드는 명령을 반복해서 실행합니다. 예를 들어 ffprobe미디어 파일에 대한 정보를 가져옵니다. 동일한 입력이 주어지면 항상 동일한 출력이 생성되어야 하므로 캐싱이 가능해야 합니다.

나는 보았다명령줄 출력 기억/캐시하지만 저는 좀 더 철저한 구현을 찾고 있습니다. 특히 구현은 단지 명령줄을 비교하는 것 같습니다. 전달된 파일 중 하나가 수정되었는지는 알 수 없습니다. (고정 길이 버퍼도 잔뜩 있어서 의심스럽기도 하고, 데몬이라는 것도 이상하네요.)

내 기사를 쓰기 시작하기 전에 기사가 존재하는지 궁금했습니다. 주요 요구사항:

  • 명령줄의 입력 파일이 변경되면 명령을 다시 실행해야 합니다.
  • 명령줄 옵션이 변경되면 명령을 다시 실행해야 합니다.
  • 나는 명령이 "비대화형" 방식으로 실행된다는 점에 동의합니다. 예를 들어 /dev/nullstdin을 사용하고 stdout 및 stderr와 같은 두 개의 다른 파일을 사용합니다.
  • 명령이 잘못되면 종료 코드와 함께 캐시하거나 전혀 캐시하지 않도록 선택할 수 있습니다.
  • 위의 내용을 고려하면 캐시된 콘텐츠는 가능한 한 자주 반환되어야 합니다. 그러나 정확성이 우선입니다.
  • 예를 들어 NFS를 통해 캐시를 여러 시스템(모두 공통 제어 하에 있음) 간에 공유할 수 있다면 더 좋을 것입니다.

기본적으로 제가 직접 작성하고 싶은 것은 (간결함을 위해 일부 잠금 및 오류 검사를 건너뛰는 것) 명령줄 + 명령줄의 각 항목에 대한 통계를 얻는 것입니다(오류 또는 dev, inode, 크기, mtime). SHA-512 또는 SHA-256을 통해 전체 혼란을 전달합니다. 이렇게 하면 고정된 크기의 키가 제공되지만 명령이나 파일이 변경되면 키도 변경됩니다(누군가가 크기와 런타임을 유지하는 변경을 하지 않는 한, 그럴 자격이 있는 경우). 키가 캐시 디렉터리에 있는지 확인하세요. 이미 존재하는 경우 해당 내용을 stdout 및 stderr에 복사하십시오. 그렇지 않으면 stdin /dev/null과 stdout 및 stderr라는 두 개의 파일을 사용하여 하위 프로세스에서 명령을 실행하십시오. 성공하면 파일을 캐시 디렉터리에 넣습니다. 그런 다음 해당 내용이 stdout 및 stderr에 복사됩니다. 결과를 직접 작성한 경우 디자인 피드백을 환영합니다. 그 결과는 자유 소프트웨어가 될 것입니다.

답변1

원하는 것이 작동하지 않아서 정말 좋은 결과를 제공하는 일반적인 도구를 찾을 수 없는 경우가 많습니다.

  • 명령줄에 없는 파일에 액세스하는 명령입니다. ( locate myfile)
  • 네트워크에 액세스하는 명령입니다. ( wget http://news.example.com/headlines)
  • 시간 의존적 명령. ( date)
  • 무작위 출력으로 명령을 내립니다. ( pwgen)

도구를 적용할 명령을 결정해야 하는 경우 필요한 것은 빌드 도구입니다. 출력이 최신이 아니더라도 명령을 실행하는 도구입니다. 고귀한 것만들다좋지 않을 것입니다. 종속성을 수동으로 정의해야 하며, 특히 다른 명령에 대한 캐시를 주의 깊게 분리해야 하고, 명령을 변경할 때 캐시를 수동으로 취소해야 하며, 각 캐시를 별도의 파일에 저장해야 합니다. 즉 불편하다. 중 하나많은 선택아마도 작업에 더 가깝습니다.SCons체크섬 기반 및 타임스탬프 기반 종속성 분석을 지원하고 그 위에 캐싱 메커니즘이 있으며 Python 코드를 작성하여 조정할 수 있습니다.

답변2

이것은 실제 답변보다 두뇌 덤프에 가깝지만 설명하기에는 너무 깁니다. 맞지 않는다면 삭제하겠습니다. 말해주세요.어깨를 으쓱하다

첫째, 가장 큰 문제는 "명령 -> 결과"라는 관점에서 생각하고 있다는 것입니다. "파일 --> 결과"인 경우 를 사용할 수 있습니다 make. 파일에서 결과로 이어지는 고정된 수의 명령만 있는 경우에도 다음을 사용할 수 있습니다. make각 명령에 대한 대상을 작성합니다.make

"모든 명령 --> 결과"여야 한다고 주장한다면 가장 먼저 떠오르는 것은 일종의 REPL 또는 Shell-in-Language-X입니다. 요즘은 부족함이 없고, 2주 정도에 한 번씩 새로운 것이 나타나는 것 같습니다. 요점은 이를 통해 다음 작업을 수행할 수 있다는 것입니다.구조화된단순한 문자열(명령)과 여러 파일이 아닌 데이터입니다.

dev+++ 의 체크섬을 얻는 것이 합리적인 것 같습니다 inode. 잘못된 긍정이 걱정된다면 언제든지 전체 비교를 수행할 수 있습니다(참고: 전체 비교는 각 파일에 대해 SHA-*를 수행하고 결과를 비교하는 것보다 항상 더 빠릅니다). 백엔드의 경우 SQLite를 사용할 수 있지만 오래된 레코드를 만료하려면 몇 가지 메커니즘이 필요합니다.sizemtime

명령 및/또는 파일의 더 많은 제한 사항을 지적할 수 있으면 더 쉬울 수 있습니다. "명령-->결과"의 완전한 범용 캐시를 달성하는 것을 목표로 하지만 여전히 입력 파일의 변경 사항을 추적하는 것은 다소 야심찬 것 같습니다.

답변3

나는 거의 같은 목적으로 내 자신의 스크립트를 작성하는 동안 이것을 발견했으며 dev+inode+size+mtime을 사용하여 파일을 캐시하는 것에 대한 당신의 아이디어가 매우 유용해 보였기 때문에 그것을 추가했습니다. 내가 이 페이지를 아주 늦게 발견하고 모든 것을 다시 작성하지 않기로 결정했기 때문에 귀하의 아이디어는 내 구현과 다릅니다.

  1. 단순화를 위해 스크립트는 캐시 항목을 단일 YAML 파일에 저장합니다. 이 파일을 여러 시스템에서 공유할 수 있지만 RCE 위험이 있으며 YAML 파일의 TOCTOU로 인해 잠금 래퍼도 작성해야 합니다.

  2. Linux에서만 실행될 수 있으며, 운이 좋다면 다른 Unix에서도 실행될 수 있습니다.

  3. 자신의 책임하에 사용하십시오. 캐시된 콘텐츠는 보호되지 않습니다.

먼저 실행하십시오 gem install chronic_duration.

#!/usr/bin/env ruby
# Usage: memoize [-D DATABASE] [-T TIMEOUT] [-F] [--] COMMAND [ARG]...
#     or memoize [-D DATABASE] --cleanup
#
# OPTIONS
#   -D DATABASE      Store entries in YAML format in DATABASE file.
#   -T TIMEOUT       Invalidate memoized entries older than TIMEOUT.
#   -F               Track file changes (dev+inode+size+mtime).
#   --cleanup        Remove all stale entries.

require 'date'
require 'optparse'
require 'digest'
require 'yaml'
require 'chronic_duration'
require 'open3'

MYSELF          = File.basename(__FILE__)
DEFAULT_DBFILE  = "#{Dir.home}/.config/memoize.yml"
DEFAULT_TIMEOUT = '1 week'

def fc(fpath) # File characteristic
  return [:dev, :ino, :size, :mtime].map do |s|
    Digest::SHA1.digest(Integer(File.stat(fpath).send(s)).to_s.b)
  end.join
end

def cmdline_checksum(cmdline, fchanges)
  pre_cksum_bytes = "".b

  cmdline.each do |c|
    characteristic   = (File.exists?(c) and fchanges) ? fc(c) : c
    pre_cksum_bytes += Digest::SHA1.digest(characteristic)
  end

  return Digest::SHA1.digest(pre_cksum_bytes)
end

def timed_out?(entry)
  return (entry[:timestamp] + Integer(entry[:timeout])) < Time.now
end

def pluralize(n, singular, plural)
  return (n % 100 == 11 || n % 10 != 1) ? plural : singular
end

fail "memoize: FATAL: this is a script, not a library" unless __FILE__ == $0

$dbfile   = DEFAULT_DBFILE
$timeout  = DEFAULT_TIMEOUT
$fchanges = false
$cleanup  = false
$retcode  = 0
$replay   = false

ARGV.options do |o|
  o.version = '2018.06.23'
  o.banner  = "Usage: memoize [OPTION]... [--] COMMAND [ARG]...\n"+
              "Cache results of COMMAND and replay its output"

  o.separator ""
  o.separator "OPTIONS"

  o.summary_indent = "  "
  o.summary_width  = 17

  o.on('-D=DATABASE', "Default: #{DEFAULT_DBFILE}")       { |d| $dbfile   = d    }
  o.on('-T=TIMEOUT',  "Default: #{DEFAULT_TIMEOUT}")      { |t| $timeout  = t    }
  o.on('-F', "Track file changes (dev+inode+size+mtime)") {     $fchanges = true }
  o.on('--cleanup', "Remove all stale entries")           {     $cleanup  = true }
end.parse!

begin
  File.open($dbfile, 'a') {}
  File.chmod(0600, $dbfile)
end unless File.exists?($dbfile)

db      = (YAML.load(File.read($dbfile)) or {})
cmdline = ARGV
cksum   = cmdline_checksum(cmdline, $fchanges)
entry   = {
  cmdline:   cmdline,
  timestamp: Time.now,
  timeout:   '1 week',
  stdout:    "",
  stderr:    "",
  retcode:   0,
}

if $cleanup
  entries = db.keys.select{|k| timed_out?(db[k]) }
  c = entries.count

  entries.each do |k|
    db.delete(k)
  end

  STDERR.puts "memoize: NOTE: #{c} stale #{pluralize(c, "entry", "entries")} removed"

  File.open($dbfile, 'w') { |f| f << YAML.dump(db) }

  exit
end

$replay = db.key?(cksum) && (not timed_out?(db[cksum]))

if $replay
  entry = db[cksum]
else
  Open3.popen3(*cmdline) do |i, o, e, t|
    i.close
    entry[:stdout]    = o.read
    entry[:stderr]    = e.read
    entry[:retcode]   = t.value.exitstatus
  end

  entry[:timestamp] = Time.now
  entry[:timeout]   = Integer(ChronicDuration.parse($timeout))
  db[cksum] = entry
end

$retcode = entry[:retcode]
STDOUT.write(entry[:stdout]) # NOTE: we don't record or replay stream timing
STDERR.write(entry[:stderr])
STDOUT.flush
STDERR.flush

File.open($dbfile, 'w') { |f| f << YAML.dump(db) }

exit! $retcode

관련 정보