--- title: "Créer un exécutable avec arguments avec Haskell" date: "2016-08-16" output: html_document: highlight: haddock keep_md: yes --- ```{r setup, include=FALSE} # use highlight option_packages <- "-package-db /home/stla/.cabal-sandbox/x86_64-linux-ghc-7.10.3-packages.conf.d" knitr::opts_chunk$set(echo = TRUE, collapse=TRUE, prompt=TRUE) Sys.setenv(PATH = "/opt/ghc/7.10.3/bin:/home/stla/bin:/bin:/usr/bin") knitr::opts_knit$set(root.dir = "./tests/testfolder/") # set global chunk options options(replace.assign=TRUE, width=90) ``` La 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 commande ```bash grep hello *.hs -n -w ``` pour chercher uniquement dans le répertoire courant, et la commande ```bash 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 exemple `hellooo` 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: ```{r include=FALSE} options(prompt="$ ") ``` ```{r engine='bash', comment=NA} tree ``` ## Exécuter une commande système dans Haskell ```{r include=FALSE} knitr::opts_chunk$set(prompt=FALSE) ``` Dans Haskell, on peut éxecuter la commande précédente ainsi : ```{r engine='haskell', engine.path='ghc', engine.opts=option_packages} import System.Process r <- readCreateProcess (shell "grep --include=\\*.hs -n -w -r -e 'hello'") "" putStrLn r ``` La fonction `readProcess` permet de passer les options à `grep` de façon plus commode, dans une liste : ```{r engine='haskell', engine.path='ghc', engine.opts=option_packages} import System.Process r <- readProcess "grep" ["--include", "*.hs", "-n", "-w", "-r", "-e", "hello"] "" putStrLn r ``` 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 : ```{r engine='haskell', engine.path='ghc', engine.opts=option_packages} import System.Process r <- readProcess "grep" ["--include", "*.hs", "-n", "-w", "-r", "-e", "xxxxx"] "" r ``` Il vaut mieux utiliser les fonctions `readCreateProcessWithExitCode` ou `readProcessWithExitCode` 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. ```{r engine='haskell', engine.path='ghc', engine.opts=option_packages} import System.Process (exitcode, stdout, stderr) <- readProcessWithExitCode "grep" ["--include", "*.hs", "-n", "-w", "-r", "-e", "hello"] "" (exitcode, stdout, stderr) ``` ```{r engine='haskell', engine.path='ghc', engine.opts=option_packages} import System.Process (exitcode, stdout, stderr) <- readProcessWithExitCode "grep" ["--include", "*.hs", "-n", "-w", "-r", "-e", "xxxxx"] "" (exitcode, stdout, stderr) ``` ## Jokers (glob) Quand on exécute la commande ```bash grep hello *.hs ``` l'interpréteur développe `*.hs` en tous les fichiers `hs` du répertoire courant. La commande qui est finalement exécutée est ```bash grep hello testfile1.hs testfile2.hs ``` Ce n'est pas `grep` qui exécute cette tâche, et de ce fait, la commande Haskell ```haskell readProcess "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: ```{r include=FALSE} knitr::opts_chunk$set(prompt=TRUE, comment=NA) ``` ```{r bash-tree, engine='bash'} tree ``` Le joker `*.hs` correspond à tous les fichiers `hs` dans le répertoire courant : ```{r engine='bash'} ls *.hs ``` Le joker `*/*.hs` correspond à tous les fichiers `hs` dans les sous-répertoires du répertoire courant : ```{r engine='bash'} ls */*.hs ``` Et ainsi de suite : ```{r engine='bash'} ls */*/*.hs ``` On peut ainsi contrôler la profondeur de la recherche quand on exécute `grep`. Plus tard, quand nous implémenterons `grep` 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` et `txt` dans les sous-dossiers : ```{r engine='bash'} ls */*.{hs,txt} ``` - fichiers qui ne se terminent pas par `t` : ```{r engine='bash'} ls *.*[!t] ``` - en mettant en marche une option au préalable, on peut aussi appliquer la négation à une chaîne de caractères: ```bash $ shopt -s extglob $ ls *.!(txt) testfile1.hs testfile2.hs ``` Cette option permet [d'autres possibilités](http://stackoverflow.com/a/217208/1100107). Revenons à Haskell. Les jokers sont implémentés dans le module `System.FilePath.Glob` de la librairie `glob`. ```{r include=FALSE} knitr::opts_chunk$set(prompt=FALSE, comment="##") ``` ```{r engine='haskell', engine.path='ghc', engine.opts=option_packages} import System.FilePath.Glob (glob) glob "*.hs" ``` La fonction `glob` retourne des chemins absolus. Si nous les utilisons avec `grep`, 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 module `Path` de la librairie `path` : ```{r engine='haskell', engine.path='ghc', engine.opts=option_packages} 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 ``` ## 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. ```haskell 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 argument `depth` qui contrôle la profondeur de la recherche. Le type de cet argument est `Maybe Int`. La recherche récursive sera exécutée si on attribue la valeur `Nothing` à `depth`. Le type `Maybe Int` sera très commode pour la suite, lorsque nous créerons l'exécutable avec des options. ```haskell 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 principale `getGrepResults` de sorte qu'elle prenne en entrée une variable `Arguments`. 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](http://stackoverflow.com/a/39049486/1100107). ```haskell 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écutable `findpattern` en compilant avec la commande `ghc findpattern.hs`. ```bash $ findpattern txt "hello" --- Results: --- testsubfolder/testsubsubfolder/testfile001.txt:1:hello world testsubfolder/testfile01.txt:1:hello world testfile1.txt:1:hello world ``` ```bash $ findpattern txt "hello" -d 1 --- Results: --- testsubfolder/testfile01.txt:1:hello world ``` La sortie est en réalité en couleur : ![](./assets/img/findPatternInFiles.png) ## Note (2016-10-12) Plutôt que d'utiliser les jokers, on peut utiliser le module `System.FilePath.Find` de la librairie `filemanip`. Pour chercher récursivement : ```{r engine='haskell', engine.path='ghc', engine.opts=option_packages} import System.FilePath.Find find always (extension ==? ".hs") "./" ``` Pour chercher jusqu'à une profondeur donnée : ```{r engine='haskell', engine.path='ghc', engine.opts=option_packages} import System.FilePath.Find find (depth <=? 1) (extension ==? ".hs") "./" ```