2011-12-22 19 views
24

Questa è più una domanda di stile, piuttosto che un modo per farlo.Haskell: Parsing argomenti della riga di comando

Quindi ho un programma che richiede due argomenti della riga di comando: una stringa e un intero.

ho implementato in questo modo:

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 

Anche se questo funziona, questa è la prima volta che ho veramente usato argomenti della riga di comando in Haskell, quindi non sono sicuro se questo è un terribilmente scomodo e modo illeggibile di fare ciò che voglio.

L'utilizzo della corrispondenza di pattern pigro funziona, ma ho potuto vedere come potrebbe essere disapprovato da altri codificatori. E l'uso delle letture per vedere se ho avuto un parse di successo mi sono sentito davvero imbarazzante quando lo scrivo.

Esiste un modo più idiomatico per fare ciò?

risposta

17

Io suggerisco di usare un case espressione:

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 

Il legame in una guardia utilizzato ecco un pattern guard, una nuova caratteristica aggiunta in Haskell 2010 (e l'estensione GHC comunemente usato prima).

Utilizzare reads come questo è perfettamente accettabile; è fondamentalmente l'unico modo per recuperare correttamente da letture non valide, almeno fino a quando non otteniamo readMaybe o qualcosa del suo simile nella libreria standard (ci sono state proposte per farlo nel corso degli anni, ma sono caduti preda di bikehedding). Utilizzando pattern matching pigro e condizionali per emulare un'espressione case è meno accettabile :)

Un'altra possibile alternativa, utilizzando l'estensione view patterns, è

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

Questo evita la rilegatura utilizzabile una sola aInteger, e mantiene il " analisi logica "vicino alla struttura dell'elenco degli argomenti. Tuttavia, non è Haskell standard (anche se l'estensione non è affatto controversa).

Per ulteriori gestione degli argomenti complessi, si potrebbe desiderare di guardare in un modulo specializzato - System.Console.GetOpt è nella base libreria standard, ma gestisce solo le opzioni (non argomento parsing), mentre cmdlib e cmdargs sono più soluzioni "full-stack" (anche se vi consiglio di evitare la modalità "Implicita" di cmdargs, in quanto è un grossissimo trucco impuro per rendere la sintassi un po 'più bella, tuttavia la modalità "Esplicita" dovrebbe andare bene).

+3

Google è tuo amico! Ecco un ottimo articolo sugli argomenti della riga di comando in Haskell: http://leiffrenzel.de/papers/commandline-options-in-haskell.html – Sanjamal

+4

@Sanjamal: Ciò non semplifica l'analisi di * argomenti *, tuttavia, solo opzioni. – ehird

+0

è anche possibile eliminare l'associazione 'args' utilizzando'CaldaCase' dando 'getArgs >> = \ case ...' invece di 'args <- getArgs; case args di ... ' – rampion

4

Ci sono un sacco di librerie argomento/opzione di analisi in Haskell che rendono la vita più facile che con read/getOpt, un esempio con una moderna (optparse-applicative) potrebbe essere di interesse:

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

sono d'accordo il optparse-applicative il pacchetto è molto carino Eccezionale! Lasciatemi dare un esempio aggiornato.

Il programma accetta come argomento una stringa e un intero n, restituisce la stringa replicata n volte e ha un flag che inverte la stringa.

-- 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 volta che il file viene compilato (con ghc come al solito):

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

Ora, supporre che si desidera un argomento opzionale, un nome da aggiungere alla fine della stringa:

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

Compile e divertirsi:

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

** Aggiornamento: ** Non ancora testato, ma penso che per ottenere un esempio aggiornato, si deve sostituire' argomento str' con 'strOption' e' argomento auto 'con' opzione auto'. –

3

In questi giorni, io sono un grande fan di optparse-generic per il parsing argomenti della riga di comando:

  • permette si analizza argomenti (non solo opzioni)
  • ti permette di analizzare le opzioni (non solo argomenti)
  • si possibile annotare gli argomenti per fornire un valido aiuto
  • ma non c'è bisogno di

Come il vostro programma di matu res, si consiglia di fornire un aiuto completo e un tipo di dati delle opzioni ben annotati, che è options-generic eccellente. Ma è anche grande a parsing liste e tuple senza alcuna annotazione a tutti, in modo da poter colpire il suolo in esecuzione:

Per esempio

{-# LANGUAGE OverloadedStrings #-} 
module Main where 

import Options.Generic 

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

viene eseguito come:

$ ./OptparseGenericExample 
Missing: INT CHAR 

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