Библиотека разбора опций командной строки 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 позволяющий задать:
- короткое и полное имя опции в формате
@s:long@ - значение по умолчанию
- строку помощи, определяющую тип аргумента
- функцию
read, которая будет применяться к строке при чтении аргумента - функцию
show, которая будет применяться к строковому представлению аргумента - строку подсказки
И две более простые функции 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) и короткой опцией-ddata 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.