Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cabal/Cabal.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ library
Distribution.Compat.Exception
Distribution.Compat.ReadP
Distribution.Compiler
Distribution.Utils.Glob
Distribution.InstalledPackageInfo
Distribution.License
Distribution.Make
Expand Down Expand Up @@ -241,6 +242,10 @@ library
Distribution.Simple.GHC.IPI641
Distribution.Simple.GHC.IPI642
Distribution.Simple.GHC.ImplInfo
Distribution.Utils.Glob.Type
Distribution.Utils.Glob.Parse
Distribution.Utils.Glob.Match
Distribution.Utils.Safe
Paths_Cabal

if flag(bundled-binary-generic)
Expand All @@ -260,6 +265,7 @@ test-suite unit-tests
UnitTests.Distribution.Compat.ReadP
UnitTests.Distribution.Simple.Program.Internal
UnitTests.Distribution.Utils.NubList
UnitTests.Distribution.Utils.Glob
main-is: UnitTests.hs
build-depends:
base,
Expand Down
6 changes: 3 additions & 3 deletions Cabal/Distribution/PackageDescription/Check.hs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import Distribution.License
import Distribution.Simple.CCompiler
( filenameCDialect )
import Distribution.Simple.Utils
( cabalVersion, intercalate, parseFileGlob, FileGlob(..), lowercase )
( cabalVersion, intercalate, parseFileGlob, isRealGlob, lowercase )

import Distribution.Version
( Version(..)
Expand Down Expand Up @@ -1098,8 +1098,8 @@ checkCabalVersion pkg =
dataFilesUsingGlobSyntax = filter usesGlobSyntax (dataFiles pkg)
extraSrcFilesUsingGlobSyntax = filter usesGlobSyntax (extraSrcFiles pkg)
usesGlobSyntax str = case parseFileGlob str of
Just (FileGlob _ _) -> True
_ -> False
Just g -> isRealGlob g
Nothing -> False

versionRangeExpressions =
[ dep | dep@(Dependency _ vr) <- buildDepends pkg
Expand Down
57 changes: 20 additions & 37 deletions Cabal/Distribution/Simple/Utils.hs
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,12 @@ module Distribution.Simple.Utils (
isInSearchPath,
addLibraryPath,

-- * simple file globbing
-- * file globbing
matchFileGlob,
matchDirFileGlob,
parseFileGlob,
FileGlob(..),
Glob(..),
isRealGlob,

-- * modification time
moreRecentFile,
Expand Down Expand Up @@ -127,6 +128,7 @@ module Distribution.Simple.Utils (
ordNubRight,
wrapText,
wrapLine,
tailMay,
) where

import Control.Monad
Expand Down Expand Up @@ -156,7 +158,7 @@ import System.Exit
import System.FilePath
( normalise, (</>), (<.>)
, getSearchPath, joinPath, takeDirectory, splitFileName
, splitExtension, splitExtensions, splitDirectories
, splitExtension, splitDirectories
, searchPathSeparator )
import System.Directory
( createDirectory, renameFile, removeDirectoryRecursive )
Expand Down Expand Up @@ -199,6 +201,8 @@ import Distribution.Compat.TempFile
import Distribution.Compat.Exception
( tryIO, catchIO, catchExit )
import Distribution.Verbosity
import Distribution.Utils.Glob
import Distribution.Utils.Safe

#ifdef VERSION_base
import qualified Paths_Cabal (version)
Expand Down Expand Up @@ -723,43 +727,22 @@ addLibraryPath os paths = addEnv
----------------
-- File globbing

data FileGlob
-- | No glob at all, just an ordinary file
= NoGlob FilePath

-- | dir prefix and extension, like @\"foo\/bar\/\*.baz\"@ corresponds to
-- @FileGlob \"foo\/bar\" \".baz\"@
| FileGlob FilePath String

parseFileGlob :: FilePath -> Maybe FileGlob
parseFileGlob filepath = case splitExtensions filepath of
(filepath', ext) -> case splitFileName filepath' of
(dir, "*") | '*' `elem` dir
|| '*' `elem` ext
|| null ext -> Nothing
| null dir -> Just (FileGlob "." ext)
| otherwise -> Just (FileGlob dir ext)
_ | '*' `elem` filepath -> Nothing
| otherwise -> Just (NoGlob filepath)

matchFileGlob :: FilePath -> IO [FilePath]
matchFileGlob = matchDirFileGlob "."

matchDirFileGlob :: FilePath -> FilePath -> IO [FilePath]
matchDirFileGlob dir filepath = case parseFileGlob filepath of
Nothing -> die $ "invalid file glob '" ++ filepath
++ "'. Wildcards '*' are only allowed in place of the file"
++ " name, not in the directory name or file extension."
++ " If a wildcard is used it must be with an file extension."
Just (NoGlob filepath') -> return [filepath']
Just (FileGlob dir' ext) -> do
files <- getDirectoryContents (dir </> dir')
case [ dir' </> file
| file <- files
, let (name, ext') = splitExtensions file
, not (null name) && ext' == ext ] of
[] -> die $ "filepath wildcard '" ++ filepath
++ "' does not match any files."
-- | Return a list of files matching a glob pattern, relative to a given source
-- directory. Note that not all the returned files are guaranteed to exist.
matchDirFileGlob :: FilePath -> String -> IO [FilePath]
matchDirFileGlob dir pattern = case parseFileGlob pattern of
Nothing ->
die $ "invalid file glob '" ++ pattern ++ "'."
Just (NoGlob filepath') ->
return [filepath']
Just (Glob glob) -> do
files <- getDirectoryContentsRecursive dir
case filter (realIsMatch glob) files of
[] -> die $ "glob pattern '" ++ pattern
++ "' does not match any files."
matches -> return matches

--------------------
Expand Down
12 changes: 12 additions & 0 deletions Cabal/Distribution/Utils/Glob.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Distribution.Utils.Glob
( Glob(..)
, isRealGlob
, parseFileGlob
, isMatch
, realIsMatch
)
where

import Distribution.Utils.Glob.Type
import Distribution.Utils.Glob.Parse
import Distribution.Utils.Glob.Match
103 changes: 103 additions & 0 deletions Cabal/Distribution/Utils/Glob/Match.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
module Distribution.Utils.Glob.Match where

import Control.Monad
( (>=>) )
import Data.Maybe
( listToMaybe )
import Data.List
( stripPrefix )
import Distribution.Utils.Safe
( tailMay )
import Distribution.Utils.Glob.Type

isMatch :: Glob -> FilePath -> Bool
isMatch (Glob realGlob) fp = realIsMatch realGlob fp
isMatch (NoGlob fp') fp = fp' == fp

realIsMatch :: RealGlob -> FilePath -> Bool
realIsMatch (RealGlob parts) fp = isMatch' True parts (toSegments fp)

toSegments :: FilePath -> [String]
toSegments = filter (not . null) . splitOn '/'

-- Not quite the same as the function from Data.List.Split, but this allows

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite the same as the function from Data.List.Split

In which way it's "not quite the same"? Why not just copy the function from Data.List.Split to Distribution.Simple.Utils?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data.List.Split.splitOn has type Eq a => [a] -> [a] -> [[a]] and allows splitting on sublists rather than just individual items. Since we only need the latter here, I thought it would make more sense to just implement this here. I guess I could explain this a bit more in the comments?

Copying the implementation from Data.List.Split would involve copying 5 or 6 separate functions, and they all have quite a lot more generality than is needed here - I think it would make this code a bit confusing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this function is equivalent to Data.List.Split.endBy - let's just call it by that name.

-- for a simpler implementation
splitOn :: Eq a => a -> [a] -> [[a]]
splitOn _ [] = []
splitOn splitter list =
let (next, rest) = span (/= splitter) list
in next : splitOn splitter (drop 1 rest)

-- | Given:
-- * A Bool which records whether we are at the beginning of the current
-- segment
-- * A list of GlobParts
-- * A list of path segments in a file path
-- Return whether the glob parts list matches the file path.
isMatch' :: Bool -> [GlobPart] -> [String] -> Bool
isMatch' _ (Literal l : parts) (seg : segs) =
case stripPrefix l seg of
Just seg' -> isMatch' False parts (seg' : segs)
Nothing -> False
isMatch' _ (PathSeparator : parts) (seg : segs)
| seg == "" = isMatch' True parts segs
| otherwise = False
isMatch' _ (CharList cs : parts) ((h:tl) : segs) =
if charListIsMatch cs h
then isMatch' False parts (tl : segs)
else False
isMatch' _ (CharListComplement cs : parts) ((h:tl) : segs) =
if charListIsMatch cs h
then False
else isMatch' False parts (tl : segs)
isMatch' startSegment (WildOne : parts) ((h:tl) : segs)
| startSegment && h == '.' = False
| otherwise = isMatch' False parts (tl : segs)
isMatch' startSegment (WildMany : parts) segs
| startSegment && (listToMaybe >=> listToMaybe) segs == Just '.' = False
| otherwise =
case segs of
first : rest ->
let candidates = map (:rest) (iterateWhile tailMay first)
in any (isMatch' False parts) candidates
[] ->
isMatch' startSegment parts segs
isMatch' startSegment (WildManyRecursive : parts) segs
| startSegment && (listToMaybe >=> listToMaybe) segs == Just '.' = False
| otherwise =
anyCandidates || handlePathSep
where
anyCandidates =
any (\(start, segs') -> isMatch' start parts segs') candidates
candidates = iterateWhile (drop1' . snd) (False, segs)
handlePathSep =
case parts of
PathSeparator : parts' -> isMatch' startSegment parts' segs
_ -> False

isMatch' startSegment (Choice gs : parts) segs =
any (\g -> isMatch' startSegment (g ++ parts) segs) gs
isMatch' _ [] [""] = True
isMatch' _ _ _ = False

charListIsMatch :: [CharListPart] -> Char -> Bool
charListIsMatch parts c = any (matches c) parts
where
matches x (CharLiteral y) = x == y
matches x (Range start end) = start <= x && x <= end

-- | Drop one character from a list of path segments, or if the first segment
-- is empty, move on to the next segment.
drop1' :: [String] -> Maybe (Bool, [String])
drop1' [] = Nothing
drop1' ("" : segs) = Just (True, segs)
drop1' (seg : segs) = Just (False, drop 1 seg : segs)

-- | Generate a list of values obtained by repeatedly applying a function

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can just use unfoldr instead of this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried this and it's a bit awkward with unfoldr. Here's the best way I could think of for reimplementing iterateWhile in terms of unfoldr:

iterateWhile f x = x : unfoldr (fmap tuple . f) x
  where
  tuple z = (z, z)

In particular, unfoldr seems to always leave us with one fewer element in the list than we want (hence the x : ...).

I also can't see a way of rewriting the call sites of iterateWhile to use unfoldr, other than to inline the above definition, which again, seems a bit awkward.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, iterate is just \f x -> unfoldr (\x' -> Just (x', f x')) x, so iterateWhile is \f x -> unfoldr (fmap (\x' -> (x', f x'))) (Just x).

Rewriting the call sites is easy if you pass in a function of type a -> Maybe (a, a) instead of a -> Maybe a. E.g. iterateWhile tailMay == unfoldr (\l -> case l of [] -> Nothing; (_:tl) -> Just (tl, tl)) == unfoldr (fmap (\l -> (l,l)) . tailMay).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last example should be iterateWhile tailMay == unfoldr (\l -> case l of [] -> Nothing; (_:tl) -> Just (l, tl)) == unfoldr (\l -> fmap (\l' -> (l, l')) . tailMay $ l).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite - iterateWhile tailMay [1,2,3] == [[1,2,3],[2,3],[3],[]] but unfoldr (\l -> fmap (\l' -> (l, l')) . tailMay $ l) [1,2,3] == [[1,2,3],[2,3],[3]].

It's just occurred to me that iterateWhile tailMay == Data.List.tails though, shall I make that replacement and remove tailMay?

That does still leave one call site of iterateWhile.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just occurred to me that iterateWhile tailMay == Data.List.tails though, shall I make that replacement and remove tailMay?

Sure, the less code, the better.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, yeah, you're right, you need something like unfoldr (fmap (\x -> (x, case x of [] -> Nothing; (_:tl) -> Just tl))) (Just [1,2,3]) to express tails with unfoldr.

-- to an initial value, until it stops returning Just.
iterateWhile :: (a -> Maybe a) -> a -> [a]
iterateWhile f x = x : rest
where
rest = case f x of
Just y -> iterateWhile f y
Nothing -> []
Loading