filepattern-0.1: src/System/FilePattern/Core.hs
-- | The type of patterns and wildcards, and operations working on parsed versions.
module System.FilePattern.Core(
FilePattern,
Pattern(..), parsePattern,
Path(..), parsePath, renderPath,
mkParts,
match, substitute,
arity
) where
import Data.Functor
import Control.Applicative
import System.FilePattern.Wildcard
import System.FilePath (isPathSeparator)
import Data.Either.Extra
import Data.Traversable
import qualified Data.Foldable as F
import System.FilePattern.Monads
import Data.List.Extra
import Prelude
-- | A type synonym for file patterns, containing @**@ and @*@. For the syntax
-- and semantics of 'FilePattern' see 'System.FilePattern.?=='.
--
-- Most 'FilePath' values lacking literal @.@ and @..@ components are suitable as 'FilePattern' values which match
-- only that specific file. On Windows @\\@ is treated as equivalent to @\/@.
--
-- You can write 'FilePattern' values as a literal string, or build them
-- up using the operators '<.>' and '</>' (but be aware that @\"\" '</>' \"foo\"@ produces @\"./foo\"@).
type FilePattern = String
newtype Path = Path [String]
deriving (Show,Eq,Ord)
newtype Pattern = Pattern (Wildcard [Wildcard String])
deriving (Show,Eq,Ord)
-- [Note: Split on ""]
--
-- For parsing patterns and paths, "" can either be [] or [""].
-- Assuming they are consistent, the only cases that are relevant are:
--
-- > match "" "" = Just []
-- > match "*" "" = if [] then Nothing else Just [""]
-- > match "**" "" = if [] then Just [] else Just [""]
--
-- We pick "" splits as [""] because that is slightly more permissive,
-- follows the builtin semantics of split, and matches the 'filepath'
-- library slightly better.
parsePath :: FilePath -> Path
parsePath = Path . split isPathSeparator
renderPath :: Path -> FilePattern
renderPath (Path x) = intercalate "/" x
parsePattern :: FilePattern -> Pattern
parsePattern = Pattern . fmap (map $ f '*') . f "**" . split isPathSeparator
where
f :: Eq a => a -> [a] -> Wildcard [a]
f x xs = case split (== x) xs of
pre:mid_post -> case unsnoc mid_post of
Nothing -> Literal pre
Just (mid, post) -> Wildcard pre mid post
-- [Note: Conversion of parts to String]
--
-- The match of * is String, but the match for ** is really [String].
-- To simplify the API, since everything else is String encoding [String],
-- we want to convert that [String] to String. We considered 3 solutions.
--
-- 1) Since we know the elements of [String] don't contain /, a natural
-- solution is to insert / characters between items with intercalate, but that
-- doesn't work because [] and [""] end up with the same representation, but
-- are very different, e.g.
--
-- > match "**/a" "a" = Just []
-- > match "**/a" "/a" = Just [""]
--
-- 2) We can join with "/" after every component, so ["a","b"] becomes
-- "a/b/". But that causes / characters to appear from nowhere, e.g.
--
-- > match "**" "a" = Just ["a/"]
--
-- 3) Logically, the only sensible encoding for [] must be "". Because [""]
-- can't be "" (would clash), it must be "/". Therefore we follow solution 2 normally,
-- but switch to solution 1 iff all the components are empty.
-- We implement this scheme with mkParts/fromParts.
--
-- Even after all that, we still have weird corner cases like:
--
-- > match "**" "/" = Just ["//"]
--
-- But the only realistic path it applies to is /, which should be pretty rare.
mkParts :: [String] -> String
mkParts xs | all null xs = replicate (length xs) '/'
| otherwise = intercalate "/" xs
fromParts :: String -> [String]
fromParts xs | all isPathSeparator xs = replicate (length xs) []
| otherwise = split isPathSeparator xs
match :: Pattern -> Path -> Maybe [String]
match (Pattern w) (Path x) = f <$> wildcardMatch (wildcardMatch equals) w x
where
f :: [Either [[Either [()] String]] [String]] -> [String]
f (Left x:xs) = rights (concat x) ++ f xs
f (Right x:xs) = mkParts x : f xs
f [] = []
substitute :: Pattern -> [String] -> Maybe Path
substitute (Pattern w) ps = do
let inner w = concat <$> wildcardSubst getNext pure w
outer w = concat <$> wildcardSubst (fromParts <$> getNext) (traverse inner) w
(ps, v) <- runNext ps $ outer w
if null ps then Just $ Path v else Nothing
arity :: Pattern -> Int
arity (Pattern x) = sum $ wildcardArity x : map wildcardArity (concat $ F.toList x)