Библиотека разбора опций командной строки hflags
date = fromGregorian 2015 jan 19
category = "Утилиты"
tags = ["опции командной строки"]
Существует много различных библиотек для разбора опций командной строки, таких как optparse-aplicative, options, и прочие. Однако в данном посте я хочу рассмотреть необычную библиотеку для разбора опций командной строки hflags.
Библиотека hflags
была написана (экс-?)работниками Google, теперь работающими в
Nilcons, на основе библиотеки flags,
для проектов на C++. Как хорошо известно в Google придерживаются мнения,
что все опции для программы должны быть опциями командной строки, и
библиотеки оптимизированы именно под такой случай.
Основной идеей hflags
предоставить работать с каждой из опций в полной
изолированности от остальных так, чтобы изменение удаление или добавление
опции не приводило к изменениям в остальных частях программы. В результате
пользователю не нужно иметь централизованный тип данных описывающий все возможные опции,
напротив, для каждый из флагов определяется деклараций в том модуле, в котором
он используется (или откуда экспортируется). По данным декларациям автоматически
формируется парсер командной строки.
Общий принцип работы
Для создания опции используется одна из возможных деклараций флага,
являющихся шаблонным методом, с помощью Template Haskell
генерируются блоки описывающие флаг, а так же описание
экземпляра класса ‘Flag’. Ниже приведен вывод -ddump-splices
, вывод
кода генерируемого Template Haskell
для флага из примеров идущих
с библиотекой:
"name" ("Indiana Jones" :: String) "Who to greet."
defineFlag ======>
:7:1-59
SimpleExample.hsdata 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
= id (HFlags.lookupFlag "name" "Main") flags_name
Здесь 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.initFlagsconst $ (const $ (const [])))
(
progDescription_a39eundefined :: HFlag_repeat),
[getFlagData (undefined :: HFlag_name)])) getFlagData (
Таким образом если не обращать внимание на 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
) и короткой опцией -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
= pure Sample
defineOptions <*> simpleOption "hello" "who" "A message to show the user."
"repeat" (3 + 4 :: Int) "Number of times to repeat the message."
defineFlag
$(return [])
main :: IO ()
= runCommand $ \opts args -> do
main $initHFlags "")
withArgs args (
greet opts
greet :: Sample -> IO ()
Sample h) = replicateM_ flags_repeat $ putStrLn $ "Hello, " ++ h greet (
В этом примере все параметры передающиеся как аргументы после символа --
будут
обработаны hflags
.