1 {-# LANGUAGE DataKinds #-}
2 {-# LANGUAGE DeriveFunctor #-}
3 {-# LANGUAGE DeriveGeneric #-}
4 {-# LANGUAGE LambdaCase #-}
5 {-# LANGUAGE RankNTypes #-}
7 -----------------------------------------------------------------------------
10 -- Module : Distribution.Simple.Glob
11 -- Copyright : Isaac Jones, Simon Marlow 2003-2004
13 -- portions Copyright (c) 2007, Galois Inc.
15 -- Maintainer : cabal-devel@haskell.org
16 -- Portability : portable
18 -- Simple file globbing.
19 module Distribution
.Simple
.Glob
23 -- * Matching on globs
30 , matchDirFileGlobWithDie
35 , GlobSyntaxError
(..)
36 , explainGlobSyntaxError
43 import Distribution
.Compat
.Prelude
46 import Distribution
.CabalSpecVersion
47 ( CabalSpecVersion
(..)
49 import Distribution
.Pretty
50 import Distribution
.Simple
.Errors
51 ( CabalException
(MatchDirFileGlob
, MatchDirFileGlobErrors
)
53 import Distribution
.Simple
.Glob
.Internal
54 import Distribution
.Simple
.Utils
57 , getDirectoryContentsRecursive
60 import Distribution
.Utils
.Path
61 import Distribution
.Verbosity
66 import Control
.Monad
(mapM)
67 import Data
.List
(stripPrefix
)
68 import System
.Directory
69 import System
.FilePath hiding ((<.>), (</>))
71 -------------------------------------------------------------------------------
75 --------------------------------------------------------------------------------
77 -- | Match a 'Glob' against the file system, starting from a
78 -- given root directory. The results are all relative to the given root.
81 matchGlob
:: FilePath -> Glob
-> IO [FilePath]
83 -- For this function, which is the general globbing one (doesn't care about
84 -- cabal spec, used e.g. for monitoring), we consider all matches.
88 GlobWarnMultiDot a
-> Just a
89 GlobMatchesDirectory a
-> Just a
90 GlobMissingDirectory
{} -> Nothing
92 <$> runDirFileGlob silent Nothing root glob
94 -- | Match a globbing pattern against a file path component
95 matchGlobPieces
:: GlobPieces
-> String -> Bool
96 matchGlobPieces
= goStart
98 -- From the man page, glob(7):
99 -- "If a filename starts with a '.', this character must be
100 -- matched explicitly."
102 go
, goStart
:: [GlobPiece
] -> String -> Bool
104 goStart
(WildCard
: _
) ('.' : _
) = False
105 goStart
(Union globs
: rest
) cs
=
107 (\glob
-> goStart
(glob
++ rest
) cs
)
109 goStart rest cs
= go rest cs
112 go
(Literal lit
: rest
) cs
113 | Just cs
' <- stripPrefix lit cs
=
116 go
[WildCard
] "" = True
117 go
(WildCard
: rest
) (c
: cs
) = go rest
(c
: cs
) || go
(WildCard
: rest
) cs
118 go
(Union globs
: rest
) cs
= any (\glob
-> go
(glob
++ rest
) cs
) globs
119 go
[] (_
: _
) = False
120 go
(_
: _
) "" = False
122 -- | Extract the matches from a list of 'GlobResult's.
124 -- Note: throws away the 'GlobMissingDirectory' results; chances are
125 -- that you want to check for these and error out if any are present.
128 globMatches
:: [GlobResult a
] -> [a
]
129 globMatches input
= [a | GlobMatch a
<- input
]
131 -- | This will 'die'' when the glob matches no files, or if the glob
132 -- refers to a missing directory, or if the glob fails to parse.
134 -- The 'Version' argument must be the spec version of the package
135 -- description being processed, as globs behave slightly differently
136 -- in different spec versions.
138 -- The first 'FilePath' argument is the directory that the glob is
139 -- relative to. It must be a valid directory (and hence it can't be
140 -- the empty string). The returned values will not include this
143 -- The second 'FilePath' is the glob itself.
147 -> Maybe (SymbolicPath CWD
(Dir dir
))
148 -> SymbolicPathX allowAbs dir file
149 -> IO [SymbolicPathX allowAbs dir file
]
150 matchDirFileGlob v
= matchDirFileGlobWithDie v dieWithException
152 -- | Like 'matchDirFileGlob' but with customizable 'die'
155 matchDirFileGlobWithDie
157 -> (forall res
. Verbosity
-> CabalException
-> IO [res
])
159 -> Maybe (SymbolicPath CWD
(Dir dir
))
160 -> SymbolicPathX allowAbs dir file
161 -> IO [SymbolicPathX allowAbs dir file
]
162 matchDirFileGlobWithDie verbosity rip version mbWorkDir symPath
=
163 let rawFilePath
= getSymbolicPath symPath
164 dir
= maybe "." getSymbolicPath mbWorkDir
165 in case parseFileGlob version rawFilePath
of
166 Left err
-> rip verbosity
$ MatchDirFileGlob
(explainGlobSyntaxError rawFilePath err
)
168 results
<- runDirFileGlob verbosity
(Just version
) dir glob
169 let missingDirectories
=
170 [missingDir | GlobMissingDirectory missingDir
<- results
]
171 matches
= globMatches results
172 directoryMatches
= [a | GlobMatchesDirectory a
<- results
]
174 let errors
:: [String]
176 [ "filepath wildcard '"
178 ++ "' refers to the directory"
181 ++ "', which does not exist or is not a directory."
182 | missingDir
<- missingDirectories
184 ++ [ "filepath wildcard '" ++ rawFilePath
++ "' does not match any files."
185 |
null matches
&& null directoryMatches
186 -- we don't error out on directory matches, simply warn about them and ignore.
191 [ "Ignoring directory '" ++ path
++ "'" ++ " listed in a Cabal package field which should only include files (not directories)."
192 | path
<- directoryMatches
197 unless (null warns
) $
200 return $ map unsafeMakeSymbolicPath matches
201 else rip verbosity
$ MatchDirFileGlobErrors errors
203 -------------------------------------------------------------------------------
205 -- * Parsing & printing
207 --------------------------------------------------------------------------------
208 -- Filepaths with globs may be parsed in the special context is globbing in
209 -- cabal package fields, such as `data-files`. In that case, we restrict the
210 -- globbing syntax to that supported by the cabal spec version in use.
211 -- Otherwise, we parse the globs to the extent of our globbing features
212 -- (wildcards `*`, unions `{a,b,c}`, and directory-recursive wildcards `**`).
214 -- ** Parsing globs in a cabal package
216 parseFileGlob
:: CabalSpecVersion
-> FilePath -> Either GlobSyntaxError Glob
217 parseFileGlob version filepath
= case reverse (splitDirectories filepath
) of
220 (filename
: "**" : segments
)
221 | allowGlobStar
-> do
222 finalSegment
<- case splitExtensions filename
of
224 |
'*' `
elem` ext
-> Left StarInExtension
225 |
null ext
-> Left NoExtensionOnStar
226 |
otherwise -> Right
(GlobDirRecursive
[WildCard
, Literal ext
])
228 | allowLiteralFilenameGlobStar
->
229 Right
(GlobDirRecursive
[Literal filename
])
231 Left LiteralFileNameGlobStar
233 foldM addStem finalSegment segments
234 |
otherwise -> Left VersionDoesNotSupportGlobStar
235 (filename
: segments
) -> do
236 pat
<- case splitExtensions filename
of
238 |
not allowGlob
-> Left VersionDoesNotSupportGlob
239 |
'*' `
elem` ext
-> Left StarInExtension
240 |
null ext
-> Left NoExtensionOnStar
241 |
otherwise -> Right
(GlobFile
[WildCard
, Literal ext
])
243 |
'*' `
elem` ext
-> Left StarInExtension
244 |
'*' `
elem` filename
-> Left StarInFileName
245 |
otherwise -> Right
(GlobFile
[Literal filename
])
247 foldM addStem pat segments
250 |
'*' `
elem` seg
= Left StarInDirectory
251 |
otherwise = Right
(GlobDir
[Literal seg
] pat
)
252 allowGlob
= version
>= CabalSpecV1_6
253 allowGlobStar
= version
>= CabalSpecV2_4
254 allowLiteralFilenameGlobStar
= version
>= CabalSpecV3_8
256 enableMultidot
:: CabalSpecVersion
-> Bool
257 enableMultidot version
258 | version
>= CabalSpecV2_4
= True
261 --------------------------------------------------------------------------------
262 -- Parse and printing utils
263 --------------------------------------------------------------------------------
265 -- ** Cabal package globbing errors
273 | LiteralFileNameGlobStar
274 | VersionDoesNotSupportGlobStar
275 | VersionDoesNotSupportGlob
278 explainGlobSyntaxError
:: FilePath -> GlobSyntaxError
-> String
279 explainGlobSyntaxError filepath StarInDirectory
=
280 "invalid file glob '"
282 ++ "'. A wildcard '**' is only allowed as the final parent"
283 ++ " directory. Stars must not otherwise appear in the parent"
285 explainGlobSyntaxError filepath StarInExtension
=
286 "invalid file glob '"
288 ++ "'. Wildcards '*' are only allowed as the"
289 ++ " file's base name, not in the file extension."
290 explainGlobSyntaxError filepath StarInFileName
=
291 "invalid file glob '"
293 ++ "'. Wildcards '*' may only totally replace the"
294 ++ " file's base name, not only parts of it."
295 explainGlobSyntaxError filepath NoExtensionOnStar
=
296 "invalid file glob '"
298 ++ "'. If a wildcard '*' is used it must be with an file extension."
299 explainGlobSyntaxError filepath LiteralFileNameGlobStar
=
300 "invalid file glob '"
302 ++ "'. Prior to 'cabal-version: 3.8'"
303 ++ " if a wildcard '**' is used as a parent directory, the"
304 ++ " file's base name must be a wildcard '*'."
305 explainGlobSyntaxError _ EmptyGlob
=
306 "invalid file glob. A glob cannot be the empty string."
307 explainGlobSyntaxError filepath VersionDoesNotSupportGlobStar
=
308 "invalid file glob '"
310 ++ "'. Using the double-star syntax requires 'cabal-version: 2.4'"
311 ++ " or greater. Alternatively, for compatibility with earlier Cabal"
312 ++ " versions, list the included directories explicitly."
313 explainGlobSyntaxError filepath VersionDoesNotSupportGlob
=
314 "invalid file glob '"
316 ++ "'. Using star wildcards requires 'cabal-version: >= 1.6'. "
317 ++ "Alternatively if you require compatibility with earlier Cabal "
318 ++ "versions then list all the files explicitly."
320 -- Note throughout that we use splitDirectories, not splitPath. On
321 -- Posix, this makes no difference, but, because Windows accepts both
322 -- slash and backslash as its path separators, if we left in the
323 -- separators from the glob we might not end up properly normalised.
326 = -- | The glob matched the value supplied.
328 |
-- | The glob did not match the value supplied because the
329 -- cabal-version is too low and the extensions on the file did
330 -- not precisely match the glob's extensions, but rather the
331 -- glob was a proper suffix of the file's extensions; i.e., if
332 -- not for the low cabal-version, it would have matched.
334 |
-- | The glob couldn't match because the directory named doesn't
335 -- exist. The directory will be as it appears in the glob (i.e.,
336 -- relative to the directory passed to 'matchDirFileGlob', and,
337 -- for 'data-files', relative to 'data-dir').
338 GlobMissingDirectory a
339 |
-- | The glob matched a directory when we were looking for files only.
340 -- It didn't match a file!
343 GlobMatchesDirectory a
344 deriving (Show, Eq
, Ord
, Functor
)
346 -- | Match files against a pre-parsed glob, starting in a directory.
348 -- The 'Version' argument must be the spec version of the package
349 -- description being processed, as globs behave slightly differently
350 -- in different spec versions.
352 -- The 'FilePath' argument is the directory that the glob is relative
353 -- to. It must be a valid directory (and hence it can't be the empty
354 -- string). The returned values will not include this prefix.
357 -> Maybe CabalSpecVersion
358 -- ^ If the glob we are running should care about the cabal spec, and warnings such as 'GlobWarnMultiDot', then this should be the version.
359 -- If you want to run a glob but don't care about any of the cabal-spec restrictions on globs, use 'Nothing'!
362 -> IO [GlobResult
FilePath]
363 runDirFileGlob verbosity mspec rawRoot pat
= do
364 -- The default data-dir is null. Our callers -should- be
365 -- converting that to '.' themselves, but it's a certainty that
366 -- some future call-site will forget and trigger a really
367 -- hard-to-debug failure if we don't check for that here.
368 when (null rawRoot
) $
370 "Null dir passed to runDirFileGlob; interpreting it "
371 ++ "as '.'. This is probably an internal error."
372 let root
= if null rawRoot
then "." else rawRoot
373 debug verbosity
$ "Expanding glob '" ++ show (pretty pat
) ++ "' in directory '" ++ root
++ "'."
374 -- This function might be called from the project root with dir as
375 -- ".". Walking the tree starting there involves going into .git/
376 -- and dist-newstyle/, which is a lot of work for no reward, so
377 -- extract the constant prefix from the pattern and start walking
378 -- there, and only walk as much as we need to: recursively if **,
379 -- the whole directory if *, and just the specific file if it's a
382 (prefixSegments
, variablePattern
) = splitConstantPrefix pat
383 joinedPrefix
= joinPath prefixSegments
385 -- The glob matching function depends on whether we care about the cabal version or not
386 doesGlobMatch
:: GlobPieces
-> String -> Maybe (GlobResult
())
387 doesGlobMatch glob str
= case mspec
of
388 Just spec
-> checkNameMatches spec glob str
389 Nothing
-> if matchGlobPieces glob str
then Just
(GlobMatch
()) else Nothing
391 go
(GlobFile glob
) dir
= do
392 entries
<- getDirectoryContents (root
</> dir
)
396 -- When running a glob from a Cabal package description (i.e.
397 -- when a cabal spec version is passed as an argument), we
398 -- disallow matching a @GlobFile@ against a directory, preferring
399 -- @GlobDir dir GlobDirTrailing@ to specify a directory match.
400 isFile
<- maybe (return True) (const $ doesFileExist (root
</> dir
</> s
)) mspec
401 let match
= (dir
</> s
<$) <$> doesGlobMatch glob s
406 Just
(GlobMatch x
) -> Just
$ GlobMatchesDirectory x
407 Just
(GlobWarnMultiDot x
) -> Just
$ GlobMatchesDirectory x
408 Just
(GlobMatchesDirectory x
) -> Just
$ GlobMatchesDirectory x
409 Just
(GlobMissingDirectory x
) -> Just
$ GlobMissingDirectory x
-- this should never match, unless you are in a file-delete-heavy concurrent setting i guess
413 go
(GlobDirRecursive glob
) dir
= do
414 entries
<- getDirectoryContentsRecursive
(root
</> dir
)
418 globMatch
<- doesGlobMatch glob
(takeFileName s
)
419 pure
((dir
</> s
) <$ globMatch
)
422 go
(GlobDir glob globPath
) dir
= do
423 entries
<- getDirectoryContents (root
</> dir
)
428 (root
</> dir
</> subdir
)
430 $ filter (matchGlobPieces glob
) entries
431 concat <$> traverse
(\subdir
-> go globPath
(dir
</> subdir
)) subdirs
432 go GlobDirTrailing dir
= return [GlobMatch dir
]
434 directoryExists
<- doesDirectoryExist (root
</> joinedPrefix
)
436 then go variablePattern joinedPrefix
437 else return [GlobMissingDirectory joinedPrefix
]
439 -- \| Extract the (possibly null) constant prefix from the pattern.
440 -- This has the property that, if @(pref, final) = splitConstantPrefix pat@,
441 -- then @pat === foldr GlobDir final pref@.
442 splitConstantPrefix
:: Glob
-> ([FilePath], Glob
)
443 splitConstantPrefix
= unfoldr' step
445 step
(GlobDir
[Literal seg
] pat
') = Right
(seg
, pat
')
446 step pat
' = Left pat
'
448 unfoldr' :: (a
-> Either r
(b
, a
)) -> a
-> ([b
], r
)
449 unfoldr' f a
= case f a
of
451 Right
(b
, a
') -> case unfoldr' f a
' of
452 (bs
, r
) -> (b
: bs
, r
)
454 -- | Is the root of this relative glob path a directory-recursive wildcard, e.g. @**/*.txt@ ?
455 isRecursiveInRoot
:: Glob
-> Bool
456 isRecursiveInRoot
(GlobDirRecursive _
) = True
457 isRecursiveInRoot _
= False
459 -- | Check how the string matches the glob under this cabal version
460 checkNameMatches
:: CabalSpecVersion
-> GlobPieces
-> String -> Maybe (GlobResult
())
461 checkNameMatches spec glob candidate
462 -- Check if glob matches in its general form
463 | matchGlobPieces glob candidate
=
464 -- if multidot is supported, then this is a clean match
465 if enableMultidot spec
466 then pure
(GlobMatch
())
467 else -- if not, issue a warning saying multidot is needed for the match
469 let (_
, candidateExts
) = splitExtensions
$ takeFileName candidate
470 extractExts
:: GlobPieces
-> Maybe String
471 extractExts
[] = Nothing
472 extractExts
[Literal lit
]
473 -- Any literal terminating a glob, and which does have an extension,
474 -- returns that extension. Otherwise, recurse until Nothing is returned.
475 |
let ext
= takeExtensions lit
478 extractExts
(_
: x
) = extractExts x
479 in case extractExts glob
of
481 | exts
== candidateExts
->
482 return (GlobMatch
())
483 | exts `
isSuffixOf` candidateExts
->
484 return (GlobWarnMultiDot
())
485 _
-> return (GlobMatch
())
488 -- | How/does the glob match the given filepath, according to the cabal version?
489 -- Since this is pure, we don't make a distinction between matching on
490 -- directories or files (i.e. this function won't return 'GlobMatchesDirectory')
491 fileGlobMatches
:: CabalSpecVersion
-> Glob
-> FilePath -> Maybe (GlobResult
())
492 fileGlobMatches version g path
= go g
(splitDirectories path
)
494 go GlobDirTrailing
[] = Just
(GlobMatch
())
495 go
(GlobFile glob
) [file
] = checkNameMatches version glob file
496 go
(GlobDirRecursive glob
) dirs
497 |
[] <- reverse dirs
=
498 Nothing
-- @dir/**/x.txt@ should not match @dir/hello@
499 | file
: _
<- reverse dirs
=
500 checkNameMatches version glob file
501 go
(GlobDir glob globPath
) (dir
: dirs
) = do
502 _
<- checkNameMatches version glob dir
-- we only care if dir segment matches