Hello, it’s been a long time.
Despite having done some progress on the resolution of the exercises of this book, I haven’t blogged about my solutions, which is a shame. The solutions are available in this git repository though.
Let’s start again for the end of chapter 8.
Here, we need to implement a case insensitive version of the glob checker.
Despite having looked on Hoogle for some elegant solution, I did not found anything suitable… I ended up writing a dirty hack.
The idea is to add a boolean flag to the matches glob. If this boolean is unset - meaning that we do not want to be case sensitive - we uppercase both the filename and the pattern.
Despite not being an elegant solution, it works.
matchesGlob :: FilePath -> String -> Bool -> Bool matchesGlob name pat True = name =~ globToRegex pat matchesGlob name pat False = map toUpper name =~ globToRegex (map toUpper pat)
Generalisation of the case sensitivity
First of all, we need a way to determine that we are on a windows system. According to Hoogle,
the isPathSeparator function seems to be a pretty good candidate. If
'\' is a path separator, then we are on a windows environment.
So first, let’s select the right glob matching func:
let globMatchingFunc = if isPathSeparator '\\' then matchesGlobCaseSensiitive else matchesGlobNotCaseSensitive
Then we’ll use this function in the filter part:
return (filter (globMatchingFunc pat) names')
You can find the entire file in order to have a higher level-view here
Replacement for doesNameExist on POSIX systems
fileExists would be a good replacement for doesNameExists.
We could use that function because everything can be seen as a file on a POSIX system.
However, using this specificity would make our software POSIX specific thus impossible to run on a windows system.
Recursive wild card implementation
In order to implement this feature, we need several parts. The first one will be a function that returns the childrens of a directory.
This feature has been implemented using the following algorithm:
getChildrenDirs :: String -> IO [String] getChildrenDirs path = getChildrenDirs' [path] where getChildrenDirs' :: [String] -> IO [String] getChildrenDirs' paths = do pathNames <- forM paths $ \path -> do dirContent <- listDirectory $ dropTrailingPathSeparator path let absDirContent = map (\p -> dropTrailingPathSeparator path </> p) dirContent dirs <- filterM doesDirectoryExist absDirContent if null dirs then return [dropTrailingPathSeparator path] else do children <- getChildrenDirs' dirs return (path:children) return (concat pathNames)
Nothing too fancy here, just a recursive tree traversal. The only new thing (at least for me at this point) was the use of filterM which let us use filter on a IO() list.
Now we have this function, we can:
- Detect the
**pattern somewhere in the pattern.
- List the subdirectories according the beginning of the path given in parameter.
- Pattern match the files contained in these folders according the basename filter.
In order to implement point 1, we’ll need to implement a simple predicate detecting the “double wildcard”:
containsDoubleWildcard :: FilePath -> Bool containsDoubleWildcard path = path =~ "^.*\\*\\*.*$"
If we detect this kind of pattern, we want to add the current dir and its children to the file lookup. In order to do that, we will add these directories to the “dirs” list in the namesMatching as follow:
dirs <- if isPattern dirName || isPattern baseName then if containsDoubleWildcard baseName then getChildrenDirs (dirName) else namesMatching $ dropTrailingPathSeparator dirName else return [dirName]
The rest of the namesMatching will take care of the pattern matching on the files contained in the dirs list.
You can find the complete solution to the exercise on this git repository.
The last question being trivial, we are done with this chapter :)