1 {-# LANGUAGE DeriveFunctor #-}
2 {-# LANGUAGE FlexibleContexts #-}
3 {-# LANGUAGE RankNTypes #-}
5 -----------------------------------------------------------------------------
7 -- Module : Distribution.Simple.Glob
8 -- Copyright : Isaac Jones, Simon Marlow 2003-2004
10 -- portions Copyright (c) 2007, Galois Inc.
12 -- Maintainer : cabal-devel@haskell.org
13 -- Portability : portable
15 -- Simple file globbing.
17 module Distribution
.Simple
.Glob
(
24 explainGlobSyntaxError
,
29 import Distribution
.Compat
.Prelude
31 import Control
.Monad
(guard)
33 import Distribution
.Simple
.Utils
34 import Distribution
.Verbosity
35 import Distribution
.Version
37 import System
.Directory
(getDirectoryContents, doesDirectoryExist, doesFileExist)
38 import System
.FilePath (joinPath
, splitExtensions
, splitDirectories
, takeFileName
, (</>), (<.>))
40 -- Note throughout that we use splitDirectories, not splitPath. On
41 -- Posix, this makes no difference, but, because Windows accepts both
42 -- slash and backslash as its path separators, if we left in the
43 -- separators from the glob we might not end up properly normalised.
47 -- ^ The glob matched the value supplied.
49 -- ^ The glob did not match the value supplied because the
50 -- cabal-version is too low and the extensions on the file did
51 -- not precisely match the glob's extensions, but rather the
52 -- glob was a proper suffix of the file's extensions; i.e., if
53 -- not for the low cabal-version, it would have matched.
54 | GlobMissingDirectory
FilePath
55 -- ^ The glob couldn't match because the directory named doesn't
56 -- exist. The directory will be as it appears in the glob (i.e.,
57 -- relative to the directory passed to 'matchDirFileGlob', and,
58 -- for 'data-files', relative to 'data-dir').
59 deriving (Show, Eq
, Ord
, Functor
)
61 -- | Extract the matches from a list of 'GlobResult's.
63 -- Note: throws away the 'GlobMissingDirectory' results; chances are
64 -- that you want to check for these and error out if any are present.
65 globMatches
:: [GlobResult a
] -> [a
]
66 globMatches input
= [ a | GlobMatch a
<- input
]
74 | LiteralFileNameGlobStar
75 | VersionDoesNotSupportGlobStar
76 | VersionDoesNotSupportGlob
79 explainGlobSyntaxError
:: FilePath -> GlobSyntaxError
-> String
80 explainGlobSyntaxError filepath StarInDirectory
=
81 "invalid file glob '" ++ filepath
82 ++ "'. A wildcard '**' is only allowed as the final parent"
83 ++ " directory. Stars must not otherwise appear in the parent"
85 explainGlobSyntaxError filepath StarInExtension
=
86 "invalid file glob '" ++ filepath
87 ++ "'. Wildcards '*' are only allowed as the"
88 ++ " file's base name, not in the file extension."
89 explainGlobSyntaxError filepath StarInFileName
=
90 "invalid file glob '" ++ filepath
91 ++ "'. Wildcards '*' may only totally replace the"
92 ++ " file's base name, not only parts of it."
93 explainGlobSyntaxError filepath NoExtensionOnStar
=
94 "invalid file glob '" ++ filepath
95 ++ "'. If a wildcard '*' is used it must be with an file extension."
96 explainGlobSyntaxError filepath LiteralFileNameGlobStar
=
97 "invalid file glob '" ++ filepath
98 ++ "'. If a wildcard '**' is used as a parent directory, the"
99 ++ " file's base name must be a wildcard '*'."
100 explainGlobSyntaxError _ EmptyGlob
=
101 "invalid file glob. A glob cannot be the empty string."
102 explainGlobSyntaxError filepath VersionDoesNotSupportGlobStar
=
103 "invalid file glob '" ++ filepath
104 ++ "'. Using the double-star syntax requires 'cabal-version: 2.4'"
105 ++ " or greater. Alternatively, for compatibility with earlier Cabal"
106 ++ " versions, list the included directories explicitly."
107 explainGlobSyntaxError filepath VersionDoesNotSupportGlob
=
108 "invalid file glob '" ++ filepath
109 ++ "'. Using star wildcards requires 'cabal-version: >= 1.6'. "
110 ++ "Alternatively if you require compatibility with earlier Cabal "
111 ++ "versions then list all the files explicitly."
113 data IsRecursive
= Recursive | NonRecursive
115 data MultiDot
= MultiDotDisabled | MultiDotEnabled
118 = GlobStem
FilePath Glob
119 -- ^ A single subdirectory component + remainder.
120 | GlobFinal GlobFinal
123 = FinalMatch IsRecursive MultiDot
String
124 -- ^ First argument: Is this a @**/*.ext@ pattern?
125 -- Second argument: should we match against the exact extensions, or accept a suffix?
126 -- Third argument: the extensions to accept.
128 -- ^ Literal file name.
130 reconstructGlob
:: Glob
-> FilePath
131 reconstructGlob
(GlobStem dir glob
) =
132 dir
</> reconstructGlob glob
133 reconstructGlob
(GlobFinal final
) = case final
of
134 FinalMatch Recursive _ exts
-> "**" </> "*" <.> exts
135 FinalMatch NonRecursive _ exts
-> "*" <.> exts
136 FinalLit path
-> path
138 -- | Returns 'Nothing' if the glob didn't match at all, or 'Just' the
139 -- result if the glob matched (or would have matched with a higher
141 fileGlobMatches
:: Glob
-> FilePath -> Maybe (GlobResult
FilePath)
142 fileGlobMatches pat candidate
= do
143 match
<- fileGlobMatchesSegments pat
(splitDirectories candidate
)
144 return (candidate
<$ match
)
146 fileGlobMatchesSegments
:: Glob
-> [FilePath] -> Maybe (GlobResult
())
147 fileGlobMatchesSegments _
[] = Nothing
148 fileGlobMatchesSegments pat
(seg
: segs
) = case pat
of
149 GlobStem dir pat
' -> do
151 fileGlobMatchesSegments pat
' segs
152 GlobFinal final
-> case final
of
153 FinalMatch Recursive multidot ext
-> do
154 let (candidateBase
, candidateExts
) = splitExtensions
(last $ seg
:segs
)
155 guard (not (null candidateBase
))
156 checkExt multidot ext candidateExts
157 FinalMatch NonRecursive multidot ext
-> do
158 let (candidateBase
, candidateExts
) = splitExtensions seg
159 guard (null segs
&& not (null candidateBase
))
160 checkExt multidot ext candidateExts
161 FinalLit filename
-> do
162 guard (null segs
&& filename
== seg
)
163 return (GlobMatch
())
167 -> String -- ^ The pattern's extension
168 -> String -- ^ The candidate file's extension
169 -> Maybe (GlobResult
())
170 checkExt multidot ext candidate
171 | ext
== candidate
= Just
(GlobMatch
())
172 | ext `
isSuffixOf` candidate
= case multidot
of
173 MultiDotDisabled
-> Just
(GlobWarnMultiDot
())
174 MultiDotEnabled
-> Just
(GlobMatch
())
175 |
otherwise = Nothing
177 parseFileGlob
:: Version
-> FilePath -> Either GlobSyntaxError Glob
178 parseFileGlob version filepath
= case reverse (splitDirectories filepath
) of
181 (filename
: "**" : segments
)
182 | allowGlobStar
-> do
183 ext
<- case splitExtensions filename
of
184 ("*", ext
) |
'*' `
elem` ext
-> Left StarInExtension
185 |
null ext
-> Left NoExtensionOnStar
186 |
otherwise -> Right ext
187 _
-> Left LiteralFileNameGlobStar
188 foldM addStem
(GlobFinal
$ FinalMatch Recursive multidot ext
) segments
189 |
otherwise -> Left VersionDoesNotSupportGlobStar
190 (filename
: segments
) -> do
191 pat
<- case splitExtensions filename
of
192 ("*", ext
) |
not allowGlob
-> Left VersionDoesNotSupportGlob
193 |
'*' `
elem` ext
-> Left StarInExtension
194 |
null ext
-> Left NoExtensionOnStar
195 |
otherwise -> Right
(FinalMatch NonRecursive multidot ext
)
196 (_
, ext
) |
'*' `
elem` ext
-> Left StarInExtension
197 |
'*' `
elem` filename
-> Left StarInFileName
198 |
otherwise -> Right
(FinalLit filename
)
199 foldM addStem
(GlobFinal pat
) segments
201 allowGlob
= version
>= mkVersion
[1,6]
202 allowGlobStar
= version
>= mkVersion
[2,4]
204 |
'*' `
elem` seg
= Left StarInDirectory
205 |
otherwise = Right
(GlobStem seg pat
)
207 | version
>= mkVersion
[2,4] = MultiDotEnabled
208 |
otherwise = MultiDotDisabled
210 -- | This will 'die'' when the glob matches no files, or if the glob
211 -- refers to a missing directory, or if the glob fails to parse.
213 -- The returned values do not include the supplied @dir@ prefix, which
214 -- must itself be a valid directory (hence, it can't be the empty
216 matchDirFileGlob
:: Verbosity
-> Version
-> FilePath -> FilePath -> IO [FilePath]
217 matchDirFileGlob verbosity version dir filepath
= case parseFileGlob version filepath
of
218 Left err
-> die
' verbosity
$ explainGlobSyntaxError filepath err
220 results
<- runDirFileGlob verbosity dir glob
221 let missingDirectories
=
222 [ missingDir | GlobMissingDirectory missingDir
<- results
]
223 matches
= globMatches results
224 -- Check for missing directories first, since we'll obviously have
225 -- no matches in that case.
226 for_ missingDirectories
$ \ missingDir
->
228 "filepath wildcard '" ++ filepath
++ "' refers to the directory"
229 ++ " '" ++ missingDir
++ "', which does not exist or is not a directory."
230 when (null matches
) $ die
' verbosity
$
231 "filepath wildcard '" ++ filepath
232 ++ "' does not match any files."
235 -- | Match files against a pre-parsed glob, starting in a directory.
237 -- The returned values do not include the supplied @dir@ prefix, which
238 -- must itself be a valid directory (hence, it can't be the empty
240 runDirFileGlob
:: Verbosity
-> FilePath -> Glob
-> IO [GlobResult
FilePath]
241 runDirFileGlob verbosity rawDir pat
= do
242 -- The default data-dir is null. Our callers -should- be
243 -- converting that to '.' themselves, but it's a certainty that
244 -- some future call-site will forget and trigger a really
245 -- hard-to-debug failure if we don't check for that here.
248 "Null dir passed to runDirFileGlob; interpreting it "
249 ++ "as '.'. This is probably an internal error."
250 let dir
= if null rawDir
then "." else rawDir
251 debug verbosity
$ "Expanding glob '" ++ reconstructGlob pat
++ "' in directory '" ++ dir
++ "'."
252 -- This function might be called from the project root with dir as
253 -- ".". Walking the tree starting there involves going into .git/
254 -- and dist-newstyle/, which is a lot of work for no reward, so
255 -- extract the constant prefix from the pattern and start walking
256 -- there, and only walk as much as we need to: recursively if **,
257 -- the whole directory if *, and just the specific file if it's a
259 let (prefixSegments
, final
) = splitConstantPrefix pat
260 joinedPrefix
= joinPath prefixSegments
262 FinalMatch recursive multidot exts
-> do
263 let prefix
= dir
</> joinedPrefix
264 directoryExists
<- doesDirectoryExist prefix
267 candidates
<- case recursive
of
268 Recursive
-> getDirectoryContentsRecursive prefix
269 NonRecursive
-> filterM (doesFileExist . (prefix
</>)) =<< getDirectoryContents prefix
270 let checkName candidate
= do
271 let (candidateBase
, candidateExts
) = splitExtensions
$ takeFileName candidate
272 guard (not (null candidateBase
))
273 match
<- checkExt multidot exts candidateExts
274 return (joinedPrefix
</> candidate
<$ match
)
275 return $ mapMaybe checkName candidates
277 return [ GlobMissingDirectory joinedPrefix
]
279 exists
<- doesFileExist (dir
</> joinedPrefix
</> fn
)
280 return [ GlobMatch
(joinedPrefix
</> fn
) | exists
]
282 unfoldr' :: (a
-> Either r
(b
, a
)) -> a
-> ([b
], r
)
283 unfoldr' f a
= case f a
of
285 Right
(b
, a
') -> case unfoldr' f a
' of
286 (bs
, r
) -> (b
: bs
, r
)
288 -- | Extract the (possibly null) constant prefix from the pattern.
289 -- This has the property that, if @(pref, final) = splitConstantPrefix pat@,
290 -- then @pat === foldr GlobStem (GlobFinal final) pref@.
291 splitConstantPrefix
:: Glob
-> ([FilePath], GlobFinal
)
292 splitConstantPrefix
= unfoldr' step
294 step
(GlobStem seg pat
) = Right
(seg
, pat
)
295 step
(GlobFinal pat
) = Left pat