-
Créer un exécutable avec arguments avec Haskell
2016-08-16
SourceLa finalité de cet article est de créer, avec Haskell, un exécutable avec des arguments et des options. Nous verrons en outre:
comment exécuter une commande système depuis Haskell ;
comment rechercher des fichiers dans Haskell avec des jokers (“glob”) ;
comment transformer des chemins absolus en chemins relatifs dans Haskell.
Recherche de fichiers contenant un motif avec
grepCette section ne concerne pas Haskell. Nous allons présenter une commande système et plus tard nous implémenterons cette commande dans Haskell.
Supposons que l’on recherche tous les fichiers de type
hscontenant la chaîne de caractères “hello”, à partir du répertoire courant. On peut utiliser la commandegrep hello *.hs -n -wpour chercher uniquement dans le répertoire courant, et la commande
grep --include=\*.hs -n -w -r -e hellopour chercher récursivement à partir dans le répertoire courant, c’est-à-dire dans le répertoire courant, ses sous-répertoires, ses sous-sous répertoires, etc.
Les options utilisées sont:
-npour afficher les numéros des lignes dans lesquelles apparaissent la chaîne de caractères recherchée ;-w(match whole word) pour rechercher “hello” en tant que mot entier (par exemplehellooon’est pas pris en compte si on n’utilise pas cette option).
On peut aussi concaténer les options :
grep --include=\*.hs -nwr -e hello.Pour les illustrations, nous nous plaçons dans un répertoire avec ce contenu:
$ tree . ├── haskell2fa660ff0110.txt ├── testfile1.hs ├── testfile1.txt ├── testfile2.hs ├── testfile2.txt └── testsubfolder ├── testfile01.hs ├── testfile01.txt ├── testfile02.hs ├── testfile02.txt └── testsubsubfolder ├── testfile001.hs ├── testfile001.txt ├── testfile002.hs └── testfile002.txt 2 directories, 13 filesExécuter une commande système dans Haskell
Dans Haskell, on peut éxecuter la commande précédente ainsi :
import System.Process r <- readCreateProcess (shell "grep --include=\\*.hs -n -w -r -e 'hello'") "" putStrLn r ## testsubfolder/testsubsubfolder/testfile001.hs:1:let x = "hello" ## testsubfolder/testfile01.hs:1:let x = "hello" ## testfile1.hs:1:let x = "hello"La fonction
readProcesspermet de passer les options àgrepde façon plus commode, dans une liste :import System.Process r <- readProcess "grep" ["--include", "*.hs", "-n", "-w", "-r", "-e", "hello"] "" putStrLn r ## testsubfolder/testsubsubfolder/testfile001.hs:1:let x = "hello" ## testsubfolder/testfile01.hs:1:let x = "hello" ## testfile1.hs:1:let x = "hello"Toutefois ces deux fonctions ont un inconvénient. Dans le cas où
grepne trouve aucun fichier correspondant, il retourne un code de sortie d’échec, et ces deux fonctions ne gèrent pas bien cette situation :import System.Process r <- readProcess "grep" ["--include", "*.hs", "-n", "-w", "-r", "-e", "xxxxx"] "" r ## *** Exception: readCreateProcess: grep "--include" "*.hs" "-n" "-w" "-r" "-e" "xxxxx" (exit 1): failedIl vaut mieux utiliser les fonctions
readCreateProcessWithExitCodeoureadProcessWithExitCodepour gérer cette situation. Avec ces fonctions on obtient un triplet contenant : le code de sortie, la sortie standard et la sortie d’erreur.import System.Process (exitcode, stdout, stderr) <- readProcessWithExitCode "grep" ["--include", "*.hs", "-n", "-w", "-r", "-e", "hello"] "" (exitcode, stdout, stderr) ## (ExitSuccess,"testsubfolder/testsubsubfolder/testfile001.hs:1:let x = \"hello\"\ntestsubfolder/testfile01.hs:1:let x = \"hello\"\ntestfile1.hs:1:let x = \"hello\"\n","")import System.Process (exitcode, stdout, stderr) <- readProcessWithExitCode "grep" ["--include", "*.hs", "-n", "-w", "-r", "-e", "xxxxx"] "" (exitcode, stdout, stderr) ## (ExitFailure 1,"","")Jokers (glob)
Quand on exécute la commande
grep hello *.hsl’interpréteur développe
*.hsen tous les fichiershsdu répertoire courant. La commande qui est finalement exécutée estgrep hello testfile1.hs testfile2.hsCe n’est pas
grepqui exécute cette tâche, et de ce fait, la commande HaskellreadProcess "grep" ["hello", "*.hs"] ""ne donnera pas le résultat escompté.
C’est ce qu’on appelle un joker (wildcard), ou “glob” en anglais.
Les jokers sont aussi utilisés par la commande
ls. Rappelons la structure du répertoire courant:$ tree . ├── haskell2fa660ff0110.txt ├── testfile1.hs ├── testfile1.txt ├── testfile2.hs ├── testfile2.txt └── testsubfolder ├── testfile01.hs ├── testfile01.txt ├── testfile02.hs ├── testfile02.txt └── testsubsubfolder ├── testfile001.hs ├── testfile001.txt ├── testfile002.hs └── testfile002.txt 2 directories, 13 filesLe joker
*.hscorrespond à tous les fichiershsdans le répertoire courant :$ ls *.hs testfile1.hs testfile2.hsLe joker
*/*.hscorrespond à tous les fichiershsdans les sous-répertoires du répertoire courant :$ ls */*.hs testsubfolder/testfile01.hs testsubfolder/testfile02.hsEt ainsi de suite :
$ ls */*/*.hs testsubfolder/testsubsubfolder/testfile001.hs testsubfolder/testsubsubfolder/testfile002.hsOn peut ainsi contrôler la profondeur de la recherche quand on exécute
grep. Plus tard, quand nous implémenteronsgrepdans notre exécutable Haskell, nous ajouterons une option pour contrôler la profondeur de la recherche.Avant de revenir à Haskell, montrons quelques autres jokers :
- fichiers
hsettxtdans les sous-dossiers :
$ ls */*.{hs,txt} testsubfolder/testfile01.hs testsubfolder/testfile01.txt testsubfolder/testfile02.hs testsubfolder/testfile02.txt- fichiers qui ne se terminent pas par
t:
$ ls *.*[!t] testfile1.hs testfile2.hs- en mettant en marche une option au préalable, on peut aussi appliquer la négation à une chaîne de caractères:
$ shopt -s extglob $ ls *.!(txt) testfile1.hs testfile2.hsCette option permet d’autres possibilités.
Revenons à Haskell. Les jokers sont implémentés dans le module
System.FilePath.Globde la librairieglob.import System.FilePath.Glob (glob) glob "*.hs" ## ## <no location info>: ## Ambiguous module name ‘System.FilePath.Glob’: ## it was found in multiple packages: ## Glob-0.7.11@Glob_7Y8nfrHts1W2PKmDbb6BTr filemanip-0.3.6.3@filem_A0TpQXIIDuPEjHKyznzEs3La fonction
globretourne des chemins absolus. Si nous les utilisons avecgrep, on obtiendra des chemins absolus aussi pour les fichiers trouvés, ce qui encombre la visibilité. On peut les transformer en chemins relatifs à l’aide du modulePathde la librairiepath:import System.FilePath.Glob (glob) import System.Directory (getCurrentDirectory) import Path (parseAbsFile, parseAbsDir, stripDir, fromRelFile) :{ let absoluteFilePathToRelativeFilePath :: FilePath -> IO( FilePath ) absoluteFilePathToRelativeFilePath file = do currentDir <- getCurrentDirectory currentAbsDir <- parseAbsDir currentDir absFile <- parseAbsFile file relFile <- stripDir currentAbsDir absFile return $ fromRelFile relFile :} absFiles <- glob "*/*.hs" mapM absoluteFilePathToRelativeFilePath absFiles ## ## <no location info>: ## Ambiguous module name ‘System.FilePath.Glob’: ## it was found in multiple packages: ## Glob-0.7.11@Glob_7Y8nfrHts1W2PKmDbb6BTr filemanip-0.3.6.3@filem_A0TpQXIIDuPEjHKyznzEs3Implémentation dans Haskell
Mettons d’abord ensemble les choses vues précédemment. Nous créons d’abord un module qui transforme des chemins absolus en chemins relatifs au répertoire courant.
module AbsoluteFilePathToRelativeFilePath where import Path (parseAbsFile, fromRelFile, parseAbsDir, stripDir) import System.Directory (getCurrentDirectory) absoluteFilePathToRelativeFilePath :: FilePath -> IO( FilePath ) absoluteFilePathToRelativeFilePath file = do currentDir <- getCurrentDirectory currentAbsDir <- parseAbsDir currentDir absFile <- parseAbsFile file relFile <- stripDir currentAbsDir absFile return $ fromRelFile relFileNous créons maintenant un module qui exécute la commande
grep. Nous mettons un argumentdepthqui contrôle la profondeur de la recherche. Le type de cet argument estMaybe Int. La recherche récursive sera exécutée si on attribue la valeurNothingàdepth. Le typeMaybe Intsera très commode pour la suite, lorsque nous créerons l’exécutable avec des options.module GetGrepResults where import System.Process (readProcessWithExitCode) import System.Exit (ExitCode) import System.FilePath.Glob (glob) import AbsoluteFilePathToRelativeFilePath runGrep :: String -> String -> Bool -> Maybe Int -> IO(ExitCode, String, String) runGrep fileType pattern wholeword depth = do let option = if wholeword then "-nw" else "-n" case depth of Nothing -> readProcessWithExitCode "grep" ([option] ++ ["--colour=always", "--include", "*." ++ fileType, "-r", "-e", pattern]) "" Just n -> do absFiles <- glob $ (foldr (++) "*." (replicate n "*/")) ++ fileType relFiles <- mapM absoluteFilePathToRelativeFilePath absFiles readProcessWithExitCode "grep" ([pattern] ++ relFiles ++ ["--colour=always", option]) "" getGrepResults :: String -> String -> Bool -> Maybe Int -> IO() getGrepResults fileType pattern wholeword depth = do (_, stdout, stderr) <- runGrep fileType pattern wholeword depth putStrLn "\n--- Results: ---\n" case stdout of "" -> putStrLn "No result" _ -> putStrLn stdoutCréation de l’exécutable
Une librairie excellente et moderne pour créer des exécutables avec arguments et options: [optparse-applicative][http://hackage.haskell.org/package/optparse-applicative].
On crée d’abord un nouveau type de données
Argumentspour les arguments et les options. Puis on modifie notre fonction principalegetGrepResultsde sorte qu’elle prenne en entrée une variableArguments.Enfin, le code se passe de trop de commentaires tant il est lisible. On trouvera d’autres examples sur la page de la librairie
optparse-applicativeet ici.module Main where import GetGrepResults import Options.Applicative import Data.Monoid data Arguments = Arguments { filetype :: String , pattern :: String , wholeword :: Bool , depth :: Maybe Int } findFiles :: Arguments -> IO() findFiles (Arguments filetype pattern w d) = getGrepResults filetype pattern w d run :: Parser Arguments run = Arguments <$> argument str ( metavar "FILETYPE" <> help "Type of the files to search in" ) <*> argument str ( metavar "PATTERN" <> help "The pattern to search" ) <*> switch ( long "wholeword" <> short 'w' <> help "Match whole word" ) <*> ( optional $ option auto ( metavar "DEPTH" <> long "depth" <> short 'd' <> help "Depth of the search (0: current directory)" )) main :: IO () main = execParser opts >>= findFiles where opts = info (helper <*> run) ( fullDesc <> progDesc "Find files containing a pattern" <> header "findpatterninfiles -- based on grep" )Ce code étant dans le fichier
findpattern.hs, on crée un fichier exécutablefindpatternen compilant avec la commandeghc findpattern.hs.$ findpattern txt "hello" --- Results: --- testsubfolder/testsubsubfolder/testfile001.txt:1:hello world testsubfolder/testfile01.txt:1:hello world testfile1.txt:1:hello world$ findpattern txt "hello" -d 1 --- Results: --- testsubfolder/testfile01.txt:1:hello worldLa sortie est en réalité en couleur :

Note (2016-10-12)
Plutôt que d’utiliser les jokers, on peut utiliser le module
System.FilePath.Findde la librairiefilemanip.Pour chercher récursivement :
import System.FilePath.Find find always (extension ==? ".hs") "./" ## ["./testfile1.hs","./testfile2.hs","./testsubfolder/testfile01.hs","./testsubfolder/testfile02.hs","./testsubfolder/testsubsubfolder/testfile001.hs","./testsubfolder/testsubsubfolder/testfile002.hs"]Pour chercher jusqu’à une profondeur donnée :
import System.FilePath.Find find (depth <=? 1) (extension ==? ".hs") "./" ## ["./testfile1.hs","./testfile2.hs","./testsubfolder/testfile01.hs","./testsubfolder/testfile02.hs"]
