Подтвердить что ты не робот

Haskell: аргументы командной строки Parsing

Это скорее вопрос стиля, а не как.

Итак, у меня есть программа, для которой нужны два аргумента командной строки: строка и целое число.

Я реализовал его следующим образом:

main = do
  [email protected](~( aString : aInteger : [] ) ) <- getArgs
  let [email protected]( ~[(n,_)] ) = reads aInteger
  if length args /= 2 || L.null parsed
    then do
      name <- getProgName
      hPutStrLn stderr $ "usage: " ++ name ++ " <string> <integer>"
      exitFailure
    else do
      doStuffWith aString n

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

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

Есть ли более идиоматический способ сделать это?

4b9b3361

Ответ 1

Я предлагаю использовать выражение case:

main :: IO ()
main = do
  args <- getArgs
  case args of
    [aString, aInteger] | [(n,_)] <- reads aInteger ->
      doStuffWith aString n
    _ -> do
      name <- getProgName
      hPutStrLn stderr $ "usage: " ++ name ++ " <string> <integer>"
      exitFailure

Связывание с защитой, используемой здесь, это защита шаблона, новая функция, добавленная в Haskell 2010 (и обычно используемое расширение GHC до этого).

Использование reads как это вполне приемлемо; это, по сути, единственный способ правильно восстановить неверные чтения, по крайней мере, до тех пор, пока мы не получим readMaybe или что-то вроде этого в стандартной библиотеке (были предложения сделать это на протяжении многих лет, но они стали жертвой bikeshedding), Использование ленивого соответствия шаблонов и условных выражений для эмуляции выражения case менее приемлемо:)

Другая возможная альтернатива, использующая расширение шаблонов просмотра, - это

case args of
  [aString, reads -> [(n,_)]] ->
    doStuffWith aString n
  _ -> ...

Это позволяет избежать одноразовой привязки aInteger и сохраняет логику синтаксического анализа близко к структуре списка аргументов. Однако это не стандартный Haskell (хотя расширение ни в коем случае не противоречиво).

Для более сложной обработки аргументов вы можете захотеть просмотреть специализированный модуль - System.Console.GetOpt находится в стандартной библиотеке base, но только обрабатывает варианты (не разбор аргументов), в то время как cmdlib и cmdargs более "полные стеки" (хотя я предупреждаю вас о том, чтобы избежать "неявного" режима cmdargs, так как это грубое нечистое взломать, чтобы сделать синтаксис немного приятнее, однако "Явный" режим должен быть прекрасным, однако).

Ответ 2

Я согласен, что пакет optparse-applicative очень приятный. Потрясающие! Позвольте мне привести современный пример.

Программа принимает в качестве аргументов строку и целое число n, возвращает строку, реплицированную n раз, и имеет флаг, который меняет строку.

-- file: repstring.hs
import Options.Applicative
import Data.Monoid ((<>))

data Sample = Sample
  { string :: String
  , n :: Int
  , flip :: Bool }

replicateString :: Sample -> IO ()
replicateString (Sample string n flip) = 
    do 
      if not flip then putStrLn repstring else putStrLn $ reverse repstring
          where repstring = foldr (++) "" $ replicate n string

sample :: Parser Sample
sample = Sample
     <$> argument str 
          ( metavar "STRING"
         <> help "String to replicate" )
     <*> argument auto
          ( metavar "INTEGER"
         <> help "Number of replicates" )
     <*> switch
          ( long "flip"
         <> short 'f'
         <> help "Whether to reverse the string" )

main :: IO ()
main = execParser opts >>= replicateString
  where
    opts = info (helper <*> sample)
      ( fullDesc
     <> progDesc "Replicate a string"
     <> header "repstring - an example of the optparse-applicative package" )

Как только файл скомпилирован (с ghc, как обычно):

$ ./repstring --help
repstring - an example of the optparse-applicative package

Usage: repstring STRING INTEGER [-f|--flip]
  Replicate a string

Available options:
  -h,--help                Show this help text
  STRING                   String to replicate
  INTEGER                  Number of replicates
  -f,--flip                Whether to reverse the string

$ ./repstring "hi" 3 
hihihi
$ ./repstring "hi" 3 -f
ihihih

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

-- file: repstring2.hs
import Options.Applicative
import Data.Monoid ((<>))
import Data.Maybe (fromJust, isJust)

data Sample = Sample
  { string :: String
  , n :: Int
  , flip :: Bool
  , name :: Maybe String }

replicateString :: Sample -> IO ()
replicateString (Sample string n flip maybeName) = 
    do 
      if not flip then putStrLn $ repstring ++ name  else putStrLn $ reverse repstring ++ name
          where repstring = foldr (++) "" $ replicate n string
                name = if isJust maybeName then fromJust maybeName else ""

sample :: Parser Sample
sample = Sample
     <$> argument str 
          ( metavar "STRING"
         <> help "String to replicate" )
     <*> argument auto
          ( metavar "INTEGER"
         <> help "Number of replicates" )
     <*> switch
          ( long "flip"
         <> short 'f'
         <> help "Whether to reverse the string" )
     <*> ( optional $ strOption 
          ( metavar "NAME"
         <> long "append"
         <> short 'a'
         <> help "Append name" ))

Скомпилируйте и получайте удовольствие:

$ ./repstring2 "hi" 3 -f -a rampion
ihihihrampion

Ответ 3

В Haskell существует множество библиотек разбора аргументов/опций, которые облегчают жизнь, чем при использовании read/getOpt, примера с современным (optparse-applicative) может представлять интерес:

import Options.Applicative

doStuffWith :: String -> Int -> IO ()
doStuffWith s n = mapM_ putStrLn $ replicate n s

parser = fmap (,)
         (argument str (metavar "<string>")) <*>
         (argument auto (metavar "<integer>"))

main = execParser (info parser fullDesc) >>= (uncurry doStuffWith)

Ответ 4

В эти дни я большой поклонник optparse-generic для анализа аргументов командной строки:

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

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

Например

{-# LANGUAGE OverloadedStrings #-}
module Main where

import Options.Generic

main :: IO ()
main = do
  (n, c) <- getRecord "Example program"
  putStrLn $ replicate n c

Работает как:

$ ./OptparseGenericExample
Missing: INT CHAR

Usage: OptparseGenericExample INT CHAR
$ ./OptparseGenericExample 5 c
ccccc