-
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
grep
Cette 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
hs
contenant la chaîne de caractères “hello
”, à partir du répertoire courant. On peut utiliser la commandegrep hello *.hs -n -w
pour chercher uniquement dans le répertoire courant, et la commande
grep --include=\*.hs -n -w -r -e hello
pour 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:
-n
pour 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 exemplehellooo
n’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 files
Exé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
readProcess
permet de passer les options àgrep
de 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ù
grep
ne 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): failed
Il vaut mieux utiliser les fonctions
readCreateProcessWithExitCode
oureadProcessWithExitCode
pour 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 *.hs
l’interpréteur développe
*.hs
en tous les fichiershs
du répertoire courant. La commande qui est finalement exécutée estgrep hello testfile1.hs testfile2.hs
Ce n’est pas
grep
qui 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 files
Le joker
*.hs
correspond à tous les fichiershs
dans le répertoire courant :$ ls *.hs testfile1.hs testfile2.hs
Le joker
*/*.hs
correspond à tous les fichiershs
dans les sous-répertoires du répertoire courant :$ ls */*.hs testsubfolder/testfile01.hs testsubfolder/testfile02.hs
Et ainsi de suite :
$ ls */*/*.hs testsubfolder/testsubsubfolder/testfile001.hs testsubfolder/testsubsubfolder/testfile002.hs
On peut ainsi contrôler la profondeur de la recherche quand on exécute
grep
. Plus tard, quand nous implémenteronsgrep
dans 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
hs
ettxt
dans 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.hs
Cette option permet d’autres possibilités.
Revenons à Haskell. Les jokers sont implémentés dans le module
System.FilePath.Glob
de 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_A0TpQXIIDuPEjHKyznzEs3
La fonction
glob
retourne 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 modulePath
de 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_A0TpQXIIDuPEjHKyznzEs3
Implé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 relFile
Nous créons maintenant un module qui exécute la commande
grep
. Nous mettons un argumentdepth
qui 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 Int
sera 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 stdout
Cré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
Arguments
pour les arguments et les options. Puis on modifie notre fonction principalegetGrepResults
de 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-applicative
et 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écutablefindpattern
en 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 world
La 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.Find
de 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"]