Memoize (кэшировать) для программ командной строки?

С расширением подсказки % N дает вам это (текущий сценарий / исходный-файл / функция).

Итак, вы можете сделать:

print -P %N

или получить его от $ {(%): -% N} .

См. Также $ funcsourcetrace для файлов стека вызовов.

6
05.05.2017, 02:42
3 ответа

Существует так много случаев, когда то, что вы хотите, не будет работать, что вы не найдете универсальный инструмент, который дает действительно хорошие результаты:

  • Команды, которые обращаются к файлам, которые не командная строка. (locate myfile)
  • Команды для доступа к сети. (wget http://news.example.com/headlines)
  • Команды, зависящие от времени. (дата)
  • Команды со случайным выводом. (pwgen)

Если вы решаете, к каким командам применить инструмент, то вам нужен инструмент сборки: инструмент, который запускает команды, если их вывод не своевременно. Почтенный make будет не очень хорош: приходится определять зависимости вручную, в частности нужно тщательно разделять кеши для разных команд и вручную отзывать кеши при смене команды, а хранить нужно каждый кеш в отдельном файле, что неудобно. Одна из многих альтернатив может быть более подходящей для этой задачи, возможно, SCons, которая поддерживает анализ зависимостей на основе контрольной суммы и временной метки, имеет механизм кэширования поверх этого и может быть настроены путем написания кода Python.

4
27.01.2020, 20:28

Это скорее выдумка, чем реальный ответ, но он слишком длинный для комментария. Если это неуместно, я удалю это. Просто дай мне знать. пожимает плечами

Во-первых, я думаю, что основная проблема в том, что вы думаете об этом в терминах "команда --> результат". Если бы это были «файл(ы) --> результат», вы могли бы просто использовать make. Если есть только небольшое фиксированное количество команд, которые ведут из файла(ов) к результатам, вы все равно можете использовать make: напишите цель make для каждой команды.

Если вы настаиваете на том, что это должна быть «произвольная команда --> результат», первое, что приходит на ум, это какой-то REPL или оболочка на языке-X. В эти дни нет недостатка в этих вещах, кажется, новые появляются каждые две недели или около того. Суть в том, что они позволят вам работать с структурированными данными, а не только со строкой (командой) и набором файлов.

Получение контрольной суммы dev + inode + size + mtime кажется разумным. Вы всегда можете выполнить полное сравнение, если беспокоитесь о ложных срабатываниях (примечание: полное сравнение всегда быстрее, чем использование SHA-* для каждого файла и сравнение результатов). Для серверной части вы можете использовать SQLite, но вам понадобится какой-то механизм для истечения срока действия старых записей.

Было бы проще, если бы вы могли указать больше ограничений на то, какими могут быть команды и/или файлы. Стремление к полностью общему кэшированию «команда -> результат», но при этом отслеживание изменений во входных файлах кажется слишком амбициозным.

1
27.01.2020, 20:28

Я обнаружил этот вопрос, когда писал свой сценарий примерно для той же цели, и ваша идея о кэшировании файлов с их dev+inode+size+mtime показалась мне очень полезной, поэтому я добавил ее. Ваша идея и моя реализация отличаются, так как я поздно наткнулся на эту страницу и решил не переписывать все:

  1. Сценарий для простоты хранит записи кэша в одном файле YAML. Вы по-прежнему можете поделиться этим файлом на нескольких машинах, но есть риск RCE, плюс вам нужно будет написать блокирующую оболочку из-за TOCTOU в файле YAML.

  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
1
27.01.2020, 20:28

Теги

Похожие вопросы