Библиотека разбора опций командной строки hflags

Существует много различных библиотек для разбора опций командной строки, таких как optparse-aplicative, options, и прочие. Однако в данном посте я хочу рассмотреть необычную библиотеку для разбора опций командной строки hflags.

Библиотека hflags была написана (экс-?)работниками Google, теперь работающими в Nilcons, на основе библиотеки flags, для проектов на C++. Как хорошо известно в Google придерживаются мнения, что все опции для программы должны быть опциями командной строки, и библиотеки оптимизированы именно под такой случай.

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

Общий принцип работы

Для создания опции используется одна из возможных деклараций флага, являющихся шаблонным методом, с помощью Template Haskell генерируются блоки описывающие флаг, а так же описание экземпляра класса ‘Flag’. Ниже приведен вывод -ddump-splices, вывод кода генерируемого Template Haskell для флага из примеров идущих с библиотекой:

    defineFlag "name" ("Indiana Jones" :: String) "Who to greet."
  ======>
    SimpleExample.hs:7:1-59
    data HFlag_name = HFlagC_name
    instance Flag HFlag_name where
      getFlagData _
        = HFlags.FlagData
            "name"
            Nothing
            (id ("Indiana Jones" :: String))
            "STRING"
            "Who to greet."
            "Main"
            (HFlagC_name
             `seq` ((GHC.IO.evaluate flags_name) >> (return GHC.Tuple.())))
    {-# NOINLINE flags_name #-}
    flags_name :: String
    flags_name = id (HFlags.lookupFlag "name" "Main")

Здесь HFlag_name = HFlagC_name уникальный тип данных соотвествующий каждому создаваемому флагу вида HFlag_<имя_модуля>_<имя_флага>, экземпляр Flag для созданного типа данных и функцию доступа к флагу. HFlags.lookupFlag — это поиск в глобальной изменяемой переменной globalFlags :: IORef (Maybe (Map String String)) завернутое в unsafePerformIO, по этой причине для функции доступа стоит прагма {-# NOINLINE #-}.

Создание экземпляра класса является основной идеей вокруг которой построена реализация данной библиотеки, поскольку все модули рекурсивно экспортируют определенные экземпляры классов типов, то в основном модуле будут находиться все экземпляры для флагов определенных в программе. Далее с помощью Template Haskell в главном модуля на основе всех существующих экземпляров строится парсер.

    initHFlags
  ======>
    \ progDescription_a39e
      -> (System.Environment.getArgs
          >>=
            (HFlags.initFlags
               (const $ (const $ (const [])))
               progDescription_a39e
               [getFlagData (undefined :: HFlag_repeat),
                getFlagData (undefined :: HFlag_name)]))

Таким образом если не обращать внимание на unsafePerformIO и глобальные переменные, а так же Template Haskell, то данная библиотека представляет очень простой и расширяемый подход для определения опций.

Немного примеров

В библиотеке существуют базовый примитив для создания опций defineCustomFlag позволяющий задать:

  1. короткое и полное имя опции в формате @s:long@
  2. значение по умолчанию
  3. строку помощи, определяющую тип аргумента
  4. функцию read, которая будет применяться к строке при чтении аргумента
  5. функцию show, которая будет применяться к строковому представлению аргумента
  6. строку подсказки

И две более простые функции defineFlag использующу методы read и show для обработки значения, и defineEQFlag позволяющую задать еще и тип аргумента.

Несколько примеров:

defineFlag "name" ("Indiana Jones"::String) "Who to greet."

создает флаг name со значением по умолчанию “Indiana Jones”

defineFlag "d:dry_run" False "Don't print anything, just exit."

создает флаг с длинной (--dry_run) и короткой опцией -d

data Color = Red | Yellow | Green deriving (Show, Read)
defineEQFlag "favorite_color" [| Yellow :: Color |] "COLOR" "Your favorite color."

создание опции использующей метод read по умолчанию и определющей тип значения.

defineCustomFlag "percent" [| 100 :: Double |] "PERCENTAGE"
  [| \s -> let p = read s
           in if 0.0 <= p && p <= 100.0
              then p
              else error "Percentage value has to be between 0 and 100."
   |]
  [| show |]

создание опции со своим методом read.

Так же все опции можно задавать и через переменные окружения, используя переменные вида HFLAG_FLAGNAME. В случае если хочется использовать другой формат переменных окружения, то авторы предлагают запускать initFlag после обработки переменных окружения функциями getEnv и setEnv из модуля System.Environment

Поскольку для генерации используется Template Haskell, то нужно помнить об изменившимся в ghc-7.8.2 поведении при обработке шаблонов и в случае использования переменных в главном модуле использовать известный workaround.

Выводы

Несмотря на достаточную ограниченность возможностей и использование небезопасных методов (впрочем не сравнимых с используемыми библиотекой cmdargs) библиотека hflags предлагает очень простой API позволяющий элегантно решить проблемы не покрываемые другими библиотеками. Тем более что hflags можно использовать и вместе с другими библиотеками, для задания дополнительных опций имеющих значение по умолчанию и рантайм конфигурации, например так:

#!/usr/bin/env runhaskell

{-# LANGUAGE TemplateHaskell, OverloadedStrings #-}

import Control.Applicative
import Control.Monad
import Options
import HFlags

import System.Environment

data Sample = Sample
  { hello :: String }

instance Options Sample where
    defineOptions = pure Sample
       <*> simpleOption "hello" "who" "A message to show the user."

defineFlag "repeat" (3 + 4 :: Int) "Number of times to repeat the message."

$(return [])

main :: IO ()
main = runCommand $ \opts args -> do
  withArgs args ($initHFlags "")
  greet opts 

greet :: Sample -> IO ()
greet (Sample h) = replicateM_ flags_repeat $ putStrLn $ "Hello, " ++ h

В этом примере все параметры передающиеся как аргументы после символа -- будут обработаны hflags.