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