haskell command-line-arguments

Haskell: análisis de argumentos de línea de comando



command-line-arguments (4)

En estos días, soy un gran fanático de optparse-generic para analizar los argumentos de la línea de comandos:

  • te permite analizar argumentos (no solo opciones)
  • te permite analizar opciones (no solo argumentos)
  • Puedes anotar los argumentos para proporcionar una ayuda útil.
  • pero no tienes que

A medida que su programa vaya madurando, es posible que desee obtener una ayuda completa y un tipo de datos de opciones bien anotado, en el que las options-generic son excelentes. Pero también es excelente para analizar listas y tuplas sin ninguna anotación, por lo que puede comenzar a ejecutar:

Por ejemplo

{-# LANGUAGE OverloadedStrings #-} module Main where import Options.Generic main :: IO () main = do (n, c) <- getRecord "Example program" putStrLn $ replicate n c

Se ejecuta como:

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

Esto es más una cuestión de estilo, en lugar de cómo hacerlo.

Así que tengo un programa que necesita dos argumentos de línea de comando: una cadena y un entero.

Lo implementé de esta manera:

main = do args@(~( aString : aInteger : [] ) ) <- getArgs let parsed@( ~[(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

Si bien esto funciona, esta es la primera vez que realmente utilizo argumentos de línea de comando en Haskell, por lo que no estoy seguro de que sea una forma horriblemente extraña e ilegible de hacer lo que quiero.

El uso de la comparación de patrones perezosos funciona, pero pude ver cómo podría ser mal visto por otros programadores. Y el uso de las lecturas para ver si obtuve un análisis exitoso definitivamente me sentí incómodo al escribirlo.

¿Hay una manera más idiomática de hacer esto?


Estoy de acuerdo en que el paquete optparse-applicative es muy bueno. ¡Increíble! Déjame dar un ejemplo actualizado.

El programa toma como argumentos una cadena y un entero n, devuelve la cadena replicada n veces y tiene un indicador que invierte la cadena.

-- 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" )

Una vez compilado el archivo (con ghc como es habitual):

$ ./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

Ahora, supongamos que desea un argumento opcional, un nombre para agregar al final de la cadena:

-- 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" ))

Compila y diviértete:

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


Hay muchas bibliotecas de análisis de argumentos / opciones en Haskell que hacen la vida más fácil que con read / getOpt , un ejemplo con uno moderno ( optparse-applicative ) puede ser de interés:

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)


Sugiero usar una expresión de 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

El enlace en un protector usado aquí es un protector de patrón , una nueva característica agregada en Haskell 2010 (y una extensión GHC comúnmente usada antes de eso).

Usar reads como esta es perfectamente aceptable; es básicamente la única forma de recuperarse adecuadamente de las lecturas no válidas, al menos hasta que obtengamos la readMaybe o algo de su estilo en la biblioteca estándar (ha habido propuestas para hacerlo a lo largo de los años, pero han sido víctimas de la motosierra). Usar la concordancia de patrones perezosos y las condiciones para emular una expresión de case es menos aceptable :)

Otra alternativa posible, usando la extensión de patrones de vista , es

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

Esto evita el enlace aInteger un solo uso y mantiene la "lógica de análisis" cerca de la estructura de la lista de argumentos. Sin embargo, no es el estándar Haskell (aunque la extensión no es de ninguna manera controvertida).

Para un manejo de argumentos más complejo, es posible que desee buscar en un módulo especializado: System.Console.GetOpt está en la biblioteca base estándar, pero solo maneja las opciones (no el análisis de argumentos), mientras que cmdlib y cmdargs son cmdargs más completas. (aunque le advierto que evite el modo "Implícito" de cmdargs, ya que es un gran truco impuro para hacer que la sintaxis sea un poco más agradable; el modo "Explícito" debería estar bien, sin embargo).