Как создать и различить глобальные параметры с помощью «optparse-Applicative»? - PullRequest
0 голосов
/ 13 декабря 2018

В моем исполняемом файле Haskell, созданном с использованием optparse-applicative, я хотел бы иметь глобальную опцию для --version наряду с глобальной опцией --help, которая доступна для всех подкоманд.Однако пример , предоставивший (см. Ниже) для добавления опции --version к CLI с подкомандами, приводит к опции --version, которая непоследовательно доступна

$ cli create --version
Invalid option `--version'

Usage: cli create NAME
  Create a thing

$ cli delete --version
0.0

и никогда не отображаетсяв справке по подкомандам

$ cli create -h
Usage: cli create NAME
  Create a thing

Available options:
  NAME                     Name of the thing to create
  -h,--help                Show this help text

$ cli delete -h
Usage: cli delete 
  Delete the thing

Available options:
  -h,--help                Show this help text

Мне бы хотелось, чтобы --version был доступен глобально и для всех подкоманд:

$ cli create -h
Usage: cli create NAME
  Create a thing

Available options:
  NAME                     Name of the thing to create
  --version                Show version
  -h,--help                Show this help text

$ cli delete -h
Usage: cli delete 
  Delete the thing

Available options:
  --version                Show version
  -h,--help                Show this help text

$ cli create --version
0.0

$ cli delete --version
0.0

Из документации не ясно, какдобиться этого.

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

$ cli create -h
Usage: cli create NAME
  Create a thing

Arguments:
  NAME                     Name of the thing to create

Global options:
  --version                Show version
  -h,--help                Show this help text

$ cli delete -h
Usage: cli delete 
  Delete the thing

Global options:
  --version                Show version
  -h,--help                Show this help text

Есть ли способ добиться этого с помощью optparse-applicative?


{-#LANGUAGE ScopedTypeVariables#-}

import Data.Semigroup ((<>))
import Options.Applicative

data Opts = Opts
    { optGlobalFlag :: !Bool
    , optCommand :: !Command
    }

data Command
    = Create String
    | Delete

main :: IO ()
main = do
    (opts :: Opts) <- execParser optsParser
    case optCommand opts of
        Create name -> putStrLn ("Created the thing named " ++ name)
        Delete -> putStrLn "Deleted the thing!"
    putStrLn ("global flag: " ++ show (optGlobalFlag opts))
  where
    optsParser :: ParserInfo Opts
    optsParser =
        info
            (helper <*> versionOption <*> programOptions)
            (fullDesc <> progDesc "optparse subcommands example" <>
             header
                 "optparse-sub-example - a small example program for optparse-applicative with subcommands")
    versionOption :: Parser (a -> a)
    versionOption = infoOption "0.0" (long "version" <> help "Show version")
    programOptions :: Parser Opts
    programOptions =
        Opts <$> switch (long "global-flag" <> help "Set a global flag") <*>
        hsubparser (createCommand <> deleteCommand)
    createCommand :: Mod CommandFields Command
    createCommand =
        command
            "create"
            (info createOptions (progDesc "Create a thing"))
    createOptions :: Parser Command
    createOptions =
        Create <$>
        strArgument (metavar "NAME" <> help "Name of the thing to create")
    deleteCommand :: Mod CommandFields Command
    deleteCommand =
        command
            "delete"
            (info (pure Delete) (progDesc "Delete the thing"))

1 Ответ

0 голосов
/ 20 декабря 2018

Насколько я знаю, это (в частности, текст справки по категориям) не очень легко сделать с optparse-applicative, поскольку это не совсем тот шаблон, который они планировали с глобальными аргументами.Если вы согласны с использованием program --global-options command --local-options (что является довольно стандартным шаблоном) вместо program command --global-and-local-options, тогда вы можете использовать подход, показанный в связанном примере:

$ ./optparse-sub-example
optparse-sub-example - a small example program for optparse-applicative with
subcommands

Usage: optparse [--version] [--global-flag] COMMAND
  optparse subcommands example

Available options:
  -h,--help                Show this help text
  --version                Show version
  --global-flag            Set a global flag

Available commands:
  create                   Create a thing
  delete                   Delete the thing

$ ./optparse-sub-example --version create
0.0
$ ./optparse-sub-example --version delete
0.0
$ ./optparse-sub-example --global-flag create HI
Created the thing named HI
global flag: True
$ ./optparse-sub-example --global-flag delete
Deleted the thing!
global flag: True

(Примечание: я бы посоветовалпридерживаясь этого подхода, поскольку «глобальные параметры перед командой» довольно стандартны).

Если вы также хотите, чтобы глобальные параметры были доступны в каждой подкоманде, у вас возникнет несколько проблем.

  1. Насколько я знаю, не существует способа повлиять на вывод текста справки, чтобы сгруппировать их отдельно в текстах справки отдельной команды.
  2. Вам понадобится некоторая пользовательская subparser -подобная функцияэто добавляет ваши глобальные параметры и объединяет их с любыми глобальными параметрами перед командой.

Для # 2, один из способов реструктурировать пример для поддержки этого может быть что-то вроде этого:

Для начала, стандартный шаблон и импорт:

{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ApplicativeDo #-}

import Data.Monoid
import Data.Semigroup ((<>))
import Options.Applicative
import Options.Applicative.Types

Opts явно разделены на optGlobals и optCommand, что упрощает работу со всеми глобальными опциямисразу, если доступно больше:

data Opts = Opts
    { optGlobals :: !GlobalOpts 
    , optCommand :: !Command
    }
data GlobalOpts = GlobalOpts { optGlobalFlag :: Bool }

GlobalOpts должно быть Semigroup и Monoid, поскольку нам необходимо объединить параметры, видимые в различных точках (до команды, после команды, так далее.).Также должно быть возможно, с соответствующими изменениями mysubparser ниже, требовать, чтобы глобальные опции давались только после команд, и пропустить это требование.

instance Semigroup GlobalOpts where
  -- Code for merging option parser results from the multiple parsers run
  -- at various different places. Note that this may be run with the default
  -- values returned by one parser (from a location with no options present)
  -- and the true option values from another, so it may be important
  -- to distinguish between "the default value" and "no option" (since "no
  -- option" shouldn't override another value provided earlier, while
  -- "user-supplied value that happens to match the default" probably should).
  --
  -- In this case this doesn't matter, since the flag being provided anywhere
  -- should be enough for it to be considered true.
  (GlobalOpts f1) <> (GlobalOpts f2) = GlobalOpts (f1 || f2)
instance Monoid GlobalOpts where
  -- Default values for the various options. These should probably match the
  -- defaults used in the option declarations.
  mempty = GlobalOpts False

Как и прежде, тип Command для представленияразличные возможные команды:

data Command
    = Create String
    | Delete

Настоящая магия: mysubparser обертывания hsubparser для добавления глобальных опций и решения их слияния.В качестве аргумента он использует синтаксический анализатор глобальных опций:

mysubparser :: forall a b. Monoid a
            => Parser a
            -> Mod CommandFields b
            -> Parser (a, b)
mysubparser globals cmds = do

Для начала он запускает глобальный синтаксический анализатор (чтобы перехватить любые глобальные переменные, заданные перед командой):

  g1 <- globals

Itзатем использует hsubparser для получения синтаксического анализатора команд и модифицирует его для анализа глобальных параметров:

  (g2, r) <- addGlobals $ hsubparser cmds

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

  pure (g1 <> g2, r)
  where 

Вспомогательная функция addGlobals:

        addGlobals :: forall c. Parser c -> Parser (a, c)

Если задано NilP, мы просто используем mempty, чтобы получить набор параметров по умолчанию:

        addGlobals (NilP x) = NilP $ (mempty,) <$> x

Важный случай: если у нас есть OptP вокруг Option, который использует CommandReader, синтаксический анализатор globals добавляется к каждому анализатору команд:

        addGlobals (OptP (Option (CmdReader n cs g) ps)) =
          OptP (Option (CmdReader n cs $ fmap go . g) ps)
          where go pi = pi { infoParser = (,) <$> globals <*> infoParser pi }

Inво всех остальных случаях либо просто используйте набор параметров по умолчанию, либо используйте наборы параметров слияния из рекурсивных Parser с в зависимости от ситуации:

        addGlobals (OptP o) = OptP ((mempty,) <$> o)
        addGlobals (AltP p1 p2) = AltP (addGlobals p1) (addGlobals p2)
        addGlobals (MultP p1 p2) =
          MultP ((\(g2, f) -> \(g1, x) -> (g1 <> g2, f x)) <$> addGlobals p1)
                (addGlobals p2)
        addGlobals (BindP p k) = BindP (addGlobals p) $ \(g1, x) ->
                                   BindP (addGlobals $ k x) $ \(g2, x') ->
                                     pure (g1 <> g2, x')

Модификации функции main довольно минимальны и в основном связаны с использованиемновый GlobalOpts.Как только парсер для GlobalOpts станет доступен, передать его на mysubparser довольно просто:

main :: IO ()
main = do
    (opts :: Opts) <- execParser optsParser
    case optCommand opts of
        Create name -> putStrLn ("Created the thing named " ++ name)
        Delete -> putStrLn "Deleted the thing!"
    putStrLn ("global flag: " ++ show (optGlobalFlag (optGlobals opts)))
  where
    optsParser :: ParserInfo Opts
    optsParser =
        info
            (helper <*> programOptions)
            (fullDesc <> progDesc "optparse subcommands example" <>
             header
                 "optparse-sub-example - a small example program for optparse-applicative with subcommands")
    versionOption :: Parser (a -> a)
    versionOption = infoOption "0.0" (long "version" <> help "Show version")
    globalOpts :: Parser GlobalOpts
    globalOpts = versionOption <*>
      (GlobalOpts <$> switch (long "global-flag" <> help "Set a global flag"))
    programOptions :: Parser Opts
    programOptions =
      uncurry Opts <$> mysubparser globalOpts (createCommand <> deleteCommand)
    createCommand :: Mod CommandFields Command
    createCommand =
        command
            "create"
            (info createOptions (progDesc "Create a thing"))
    createOptions :: Parser Command
    createOptions =
        Create <$>
        strArgument (metavar "NAME" <> help "Name of the thing to create")
    deleteCommand :: Mod CommandFields Command
    deleteCommand =
        command
            "delete"
            (info (pure Delete) (progDesc "Delete the thing"))

Обратите внимание, что mysubparser должен быть довольно универсальным / повторно используемым компонентом.

Thisдемонстрирует поведение ближе к тому, что вы хотели:

$ ./optparse-sub-example create --global-flag HI
Created the thing named HI
global flag: True
$ ./optparse-sub-example --global-flag create HI
Created the thing named HI
global flag: True
$ ./optparse-sub-example --global-flag delete
Deleted the thing!
global flag: True
$ ./optparse-sub-example delete --global-flag
Deleted the thing!
global flag: True
$ ./optparse-sub-example delete
Deleted the thing!
global flag: False
$ ./optparse-sub-example delete --version
0.0
$ ./optparse-sub-example create --version
0.0
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...