"Ботинок"

Вы знаете, Clojure просто не так часто используется для написания скриптов, как следовало бы. И это обидно. Clojure краток, удобочитаем и чрезвычайно полезен: отличная квалификация для любого языка сценариев.

Распространенные операции в области сценариев, такие как чтение и запись файлов или выполнение HTTP-запросов, с Clojure несложно… но, в отличие от Bash, здесь действительно бесконечная мощность, когда вам это действительно нужно.

Но есть и проблемы с использованием Clojure для написания сценариев: JVM как бы мешает. А создание сценариев часто означает анализ параметров командной строки, так что теперь вам нужно с этим разобраться. И для всего интересного, что вы делаете, похоже, нужны библиотеки Clojure или Java, так что пора внести что-то, что понимает, как загружать зависимости Maven и получать их в пути к классам. И прежде чем вы это узнаете, вы уже используете Leiningen (инструмент для сборки) или просто вскидываете руки и возвращаетесь к Ruby или Python. Или Баш.

И это обидно. Потому что вы упустили из виду действительно хорошую, хорошо продуманную альтернативу: Boot.

Boot действительно предлагает способ просто добиться цели в Clojure. Вы можете написать минимальный объем кода на чистом, простом Clojure и бесплатно получить параметры командной строки и сложную среду выполнения.

Загрузка масштабируется от разовых сценариев до очень сложных конвейеров публикации. И для этого используется всего пара основных концепций: составные задачи и неизменяемые наборы файлов.

Инструменты сборки обычно декларативны: вы описываете свои цели и структуру проекта, а инструмент (Maven, Gradle, даже Make) пытается выяснить, какие команды и в каком порядке запускать для достижения вашей цели.

Загрузка меняет это почти полностью: нет иерархии целей или множества плагинов: только задачи. Нет упорядочивания зависимостей: вы указываете задачи для выполнения и порядок выполнения. Boot просто упрощает определение этих задач и их объединение. И, конечно же, он имеет дело с артефактами Maven и путем к классам, поэтому вы можете использовать любые библиотеки или даже пакеты заранее написанных задач, не задумываясь.

Много было сказано о силе возможности комбинирования; часто в терминах Unix-процессов, соединенных трубами. Если задуматься, конвейер - это последовательная неизменяемая структура данных… но очень ограниченная. Boot сохраняет идею небольших сфокусированных команд (задач), но заменяет данные, передаваемые между этими задачами, неизменяемым набором файлов (который также является постоянной записью Clojure).

Набор файлов - это набор каталогов и файлов, которые можно рассматривать как неизменное значение Clojure. Задача может управлять набором файлов, например, для добавления новых каталогов и файлов в набор файлов: как и любое хорошее значение Clojure, это приводит к новому значению набора файлов без аннулирования старого значения набора файлов.

Это немного сложное предложение, потому что набор файлов - это половина набора данных в памяти и половина каталогов и файлов в файловой системе. С практической точки зрения (и упрощенно!) Boot создает каталог файловой системы для поддержки набора файлов и управляет им. При изменении набора файлов создается новый каталог. Новый каталог содержит все новые или измененные файлы, а также жесткие ссылки на неизмененные файлы.

Задачи вписываются в этот шаблон как агенты, которые манипулируют набором файлов и, иногда, выполняют другие побочные эффекты за пределами набора файлов. Например, встроенная задача javac существует для запуска компилятора Java (это побочный эффект) и добавления полученных файлов .class в набор файлов (для последующей упаковки в файл JAR).

Задачи объединяются в конвейер. При запуске загрузка анализирует параметры командной строки и использует их для определения задач, которые необходимо выполнить. Например:

> boot pom jar install

Это вызывает загрузку, которая заботится о настройке пути к классам. Он определяет три задачи: pom, jar и install. В этом примере параметры по умолчанию для каждой задачи выполняют свою работу. Набор файлов инициализируется загрузкой, затем проходит через задачу pom, затем задачу jar, а затем задачу установки.

Задачи также могут принимать параметры командной строки:

> boot  pom --project example --version 0.1 -- jar --  install
Writing pom.xml and pom.properties...
Writing example-0.1.jar...
Installing example-0.1.jar...

Здесь у задачи pom есть параметры командной строки: —-project и --version. Использование двойных дефисов для разделения задач здесь не обязательно (но все же рекомендуется для удобства чтения).

Каждая задача - это функция, которая принимает параметры командной строки и возвращает промежуточное ПО. Каждое промежуточное программное обеспечение принимает обработчик и возвращает новый обработчик: каждый обработчик принимает и возвращает набор файлов.

Кажется, это много, но это похоже на знакомый подход, используемый в Ring, где обработчики принимают карты запросов и возвращают карты ответов.

Промежуточное ПО для всех задач составляется с использованием clojure.core / comp и вызывается для создания конвейера. Значение, которое проходит через конвейер, - это набор файлов.

На практике Boot предоставляет макросы, которые упрощают и упрощают работу.

Привет, мир в загрузке:

;; build.boot
(deftask hello
  "Prints a greeting."
  [g greeting GREETING str "Greeting to use."
   n name NAME str "name to greet."]
  (with-pass-thru _
    (info "%s, %s\n" greeting name)))

(task-options! hello {:greeting "Hello" :name "Howard"})

При запуске загрузки он ищет файл с именем build.boot в текущем каталоге. Этот файл просто содержит код Clojure с автоматически импортированными несколькими пространствами имен. В приведенном выше примере определяется задача hello и предоставляются параметры по умолчанию для задачи.

Использование with-pass-thru в реализации задачи указывает, что эта задача принимает набор файлов, но не изменяет его. Мы следуем идиоме Clojure и назначаем набор файлов _, поскольку реализация задачи даже не использует набор файлов.

Вектор в начале определения задачи объявляет параметры командной строки. -g / —-greetingoption является строкой, а параметр -n / --name также является строкой (Boot поддерживает несколько разных типов для параметров помимо строки). Boot использует эту информацию для помощи в решении задачи:

> boot hello --help
Prints a greeting.

Options:
  -h, --help               Print this help info.
  -g, --greeting GREETING  GREETING sets greeting to use.
  -n, --name NAME          NAME sets name to greet.

Команда загрузки может запустить эту задачу, используя либо параметры задачи по умолчанию, либо параметры, указанные в командной строке:

> boot hello
Hello, Howard
> boot hello -g Bonjour
Bonjour, Howard
>

Вы также можете запустить Boot REPL и использовать функцию загрузки для создания и запуска конвейеров:

> boot repl
nREPL server started on port 62614 on host 127.0.0.1 - nrepl://127.0.0.1:62614
REPL-y 0.3.7, nREPL 0.2.12
Clojure 1.9.0-alpha10
Java HotSpot(TM) 64-Bit Server VM 1.8.0_74-b02
        Exit: Control+D or (exit) or (quit)
    Commands: (user/help)
        Docs: (doc function-name-here)
              (find-doc "part-of-name-here")
Find by Name: (find-name "part-of-name-here")
      Source: (source function-name-here)
     Javadoc: (javadoc java-object-or-class-here)
    Examples from clojuredocs.org: [clojuredocs or cdoc]
              (user/clojuredocs name-here)
              (user/clojuredocs "ns-here" "name-here")
boot.user=> (boot (hello))
Hello, Howard
nil
boot.user=> (boot (hello "-n" "Clojarians"))
Hello, Clojarians
nil
boot.user=> (boot (hello :name "Booters"))
Hello, Booters
nil
boot.user=> (boot "hello" "-n" "Medium" "--" "hello" "-g" "Welcome")
Hello, Medium
Welcome, Howard
nil
boot.user=>

В этом коротком примере много чего происходит.

Аргументы функции загрузки могут быть либо строками, как из командной строки, либо функциями промежуточного программного обеспечения загрузки (готовыми к компоновке с помощью comp).

Как видите, Boot также дает вам большую гибкость: функция задачи может принимать либо ключевое слово для идентификации каждого аргумента, либо короткую или длинную строку имени параметра формы.

Я делаю некоторую работу с помощью Docker, где хочу заполнить образ Docker артефактами из репозитория Maven. Полный объем этого выходит за рамки этой статьи, но один пример задачи отражает немного больше использования Boot в гневе:

(deftask init
  "First step when building a Docker image. Optionally specifies a directory
  of resources that can be added to the image."
  [d dir DIR file "Directory to add."
   f from IMAGE str "Base image name."]
  (assert from "--from is required")
  (assert (or (nil? dir)
              (is-readable-directory? dir))
          "--dir must specify an existing directory")
  (with-pre-wrap fs
    (cond-> (-> fs
                (rm (user-files fs))
                (df/edit df/instruction :preamble :from from))
      dir (add-resource dir)
      true commit!)))

Эта задача инициализации является первой в конвейере (в конвейере следуют дальнейшие задачи для уточнения содержимого образа Docker, затем файловая задача для записи файла Docker и вызова докера для создания образа). Поскольку эта задача изменяет набор файлов, в ней используется макрос with-pre-wrap; ему передается набор файлов, и он должен возвращать измененный набор файлов.

Выражения (add-resource dir) и commit! важны: когда каталог предоставляется с использованием параметра —-dir, он добавляется в набор файлов как каталог ресурсов. После изменения набора файлов необходимо commit! набор файлов, который синхронизирует структуру каталогов в реальной файловой системе.

Еще одним важным аспектом загрузки является то, что задачи - это функции, возвращающие функции: это позволяет легко определять новые задачи в терминах конвейеров существующих задач:

(deftask base-image
  "Builds the base image for other images."
  []
  (comp (init :dir (io/file "base") :from "anapsix/alpine-java:8")
        (add :file ["launch.sh"])
        (artifact :dependency '[[org.bouncycastle/bcprov-jdk15on "1.54"]] :target "/opt/jdk/jre/lib/ext/")
        (artifact :dependency '[[com.walmartlabs/timestamper "0.1.2"]] :target "/usr/local/java-agents/")
        (inst :section :postamble :inst :run :arguments ["chmod a+x launch.sh"])
        (build-image :image-name "base" :version default-base-version)))

Это определяет базовый образ с точки зрения других задач: инициализация, добавление, артефакт, установка и, наконец, сборка образа. В этом простом примере и в Boot в целом происходит гораздо больше, чем можно здесь описать.

В итоге: загрузка - это именно то, что мне нужно для моего конкретного приложения. Скорее всего, у вас есть некоторая работа, которую вы хотели автоматизировать, но просто не могли столкнуться с перспективой выполнения этой работы в Bash или Python. Попробуйте Boot, я думаю, вы останетесь довольны!