{-| Module : Control.Lens.FileSystem Description : Lensy File system combinators Copyright : (c) Chris Penner, 2019 License : BSD3 Note that this package is experimental, test things carefully before performing destructive operations. I'm not responsible if things go wrong. This package is meant to be used alongside combinators from 'lens-action'; for example '^!', '^!!' and 'act'. -} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE FlexibleContexts #-} module Control.Lens.FileSystem ( -- * File System Helpers ls , ls'ed , path , pathL , branching , dirs , files , contents , exts , crawled , crawling , absolute , withPerms , symLinksFollowed -- * Combinators , filteredM , merging , including -- ** Exception Handling , recovering , tryOrContinue , tryCatch -- * Re-exports , () , readable , writable , executable , module System.FilePath.Lens ) where import Control.Lens import Control.Lens.Action import Control.Lens.FileSystem.Internal.Combinators import System.Directory import System.FilePath.Posix import System.FilePath.Lens -- | List the files at a given directory -- If the focused path isn't a directory this fold will return 0 results -- -- >>> "./test/data" ^! ls -- ["./test/data/flat","./test/data/symlinked","./test/data/.dotfile","./test/data/permissions","./test/data/nested"] ls :: Monoid r => Acting IO r FilePath [FilePath] ls = recovering $ act (\fp -> (fmap (fp )) <$> listDirectory fp) -- | Fold over all files in the given directory. -- If the focused path isn't a directory this fold will return 0 results -- This is an alias for @@ls . traversed@@ -- -- >>> "./test/data" ^!! ls'ed -- ["./test/data/flat","./test/data/symlinked","./test/data/.dotfile","./test/data/permissions","./test/data/nested"] ls'ed :: Monoid r => Acting IO r FilePath FilePath ls'ed = ls . traversed -- | Append a path the end of the current path. -- This uses `` for cross platform compatibility so -- you don't need leading/trailing slashes here -- -- >>> "./src" ^! path "Control" -- "./src/Control" path :: FilePath -> Getter FilePath FilePath path filePath = to ( filePath) -- | Create a filepath from a list of path segments, then append it to the focused path. -- -- >>> "." ^! pathL ["a", "b", "c"] -- "./a/b/c" pathL :: [FilePath] -> Getter FilePath FilePath pathL filePaths = to ( joinPath filePaths) -- | "Branch" a fold into many sub-paths. -- E.g. if we want to crawl into BOTH of @src@ and @test@ directories we can do: -- -- >>> "." ^!! branching ["src", "test"] . ls -- [["./src/Control"],["./test/Spec.hs","./test/data"]] branching :: [FilePath] -> Fold FilePath FilePath branching filePaths = folding (\fp -> (fp ) <$> filePaths) -- | Filter for only paths which point to a valid directory -- -- >>> "./test" ^!! ls'ed -- ["./test/Spec.hs","./test/data"] -- -- >>> "./test" ^!! ls'ed . dirs -- ["./test/data"] dirs :: (Monoid r) => Acting IO r FilePath FilePath dirs = filteredM doesDirectoryExist -- | Filter for only paths which point to a valid file -- -- >>> "./test" ^!! ls'ed -- ["./test/Spec.hs","./test/data"] -- -- >>> "./test" ^!! ls'ed . files -- ["./test/Spec.hs"] files :: (Monoid r) => Acting IO r FilePath FilePath files = filteredM doesFileExist -- | Get the contents of a file -- This fold will return 0 results if the path does not exist, if it isn't a file, or if -- reading the file causes any exceptions. -- -- This fold lifts the path of the current file into the index of the fold in case you need it -- downstream. -- -- >>> "./test/data/flat/file.md" ^! contents -- "markdown\n" -- -- >>> "./test/data/flat/file.md" ^! contents . withIndex -- ("./test/data/flat/file.md","markdown\n") contents :: (Indexable FilePath p, Effective IO r f, Monoid r) => Over' p f FilePath String contents = recovering (iact go) where go fp = do contents' <- readFile fp return (fp, contents') -- | Filter the fold for only files which have ANY of the given file extensions. -- E.g. to find all Haskell or Markdown files in the current directory: -- -- >>> "./test/" ^!! crawled . exts ["hs", "md"] -- ["./test/Spec.hs","./test/data/flat/file.md","./test/data/symlinked/file.md"] exts :: [String] -> Traversal' FilePath FilePath exts extList = filtered check where check fp = drop 1 (takeExtension fp) `elem` extList -- | Crawl over every file AND directory in the given path. -- -- >>> "./test/data/nested/top" ^!! crawled -- ["./test/data/nested/top","./test/data/nested/top/mid","./test/data/nested/top/mid/bottom","./test/data/nested/top/mid/bottom/floor.txt"] crawled :: Monoid r => Acting IO r FilePath FilePath crawled = including (dirs . ls . traversed . crawled) -- | Continually run the given fold until all branches hit dead ends, -- yielding over all elements encountered the way. -- -- >>> "./test/data" ^!! crawling (ls'ed . filtered ((== "flat") . view filename)) -- ["./test/data","./test/data/flat"] crawling :: Monoid r => Acting IO r FilePath FilePath -> Acting IO r FilePath FilePath crawling fld = including (recovering (fld . crawling fld)) -- | Make filepaths absolute in reference to the current working directory -- -- > >>> "./test/data" ^! absolute -- > "/Users/chris/dev/lens-filesystem/test/data" absolute :: MonadicFold IO FilePath FilePath absolute = act makeAbsolute -- | Filter for only paths which have ALL of the given file-permissions -- See 'readable', 'writable', 'executable' -- -- >>> "./test/data" ^!! crawled . withPerms [readable, executable] -- ["./test/data/permissions/exe"] withPerms :: Monoid r => [Permissions -> Bool] -> Acting IO r FilePath FilePath withPerms permChecks = filteredM checkAll where checkAll fp = do perms <- getPermissions fp return $ all ($ perms) permChecks -- | If the path is a symlink, rewrite the path to its destination and keep folding -- If it's not a symlink; pass the path onwards as is. -- -- >>> "./test/data/symlinked" ^! symLinksFollowed -- "flat" symLinksFollowed :: Monoid r => Acting IO r FilePath FilePath symLinksFollowed = tryOrContinue (act getSymbolicLinkTarget)