directory-contents 0.1.0.0 → 0.2.0.0
raw patch · 8 files changed
+494/−98 lines, 8 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- System.Directory.Contents: instance Data.Foldable.Foldable System.Directory.Contents.DirTree
- System.Directory.Contents: instance Data.Foldable.Foldable System.Directory.Contents.Symlink
- System.Directory.Contents: instance Data.Traversable.Traversable System.Directory.Contents.DirTree
- System.Directory.Contents: instance Data.Traversable.Traversable System.Directory.Contents.Symlink
- System.Directory.Contents: instance GHC.Base.Functor System.Directory.Contents.DirTree
- System.Directory.Contents: instance GHC.Base.Functor System.Directory.Contents.Symlink
- System.Directory.Contents: instance GHC.Classes.Eq a => GHC.Classes.Eq (System.Directory.Contents.DirTree a)
- System.Directory.Contents: instance GHC.Classes.Eq a => GHC.Classes.Eq (System.Directory.Contents.Symlink a)
- System.Directory.Contents: instance GHC.Classes.Ord a => GHC.Classes.Ord (System.Directory.Contents.DirTree a)
- System.Directory.Contents: instance GHC.Classes.Ord a => GHC.Classes.Ord (System.Directory.Contents.Symlink a)
- System.Directory.Contents: instance GHC.Generics.Generic (System.Directory.Contents.DirTree a)
- System.Directory.Contents: instance GHC.Generics.Generic (System.Directory.Contents.Symlink a)
- System.Directory.Contents: instance GHC.Read.Read a => GHC.Read.Read (System.Directory.Contents.DirTree a)
- System.Directory.Contents: instance GHC.Read.Read a => GHC.Read.Read (System.Directory.Contents.Symlink a)
- System.Directory.Contents: instance GHC.Show.Show a => GHC.Show.Show (System.Directory.Contents.DirTree a)
- System.Directory.Contents: instance GHC.Show.Show a => GHC.Show.Show (System.Directory.Contents.Symlink a)
+ System.Directory.Contents: fileName :: DirTree a -> FileName
+ System.Directory.Contents: fileNameMap :: [DirTree a] -> Map FileName (DirTree a)
+ System.Directory.Contents: insertSibling :: DirTree a -> Map FileName (DirTree a) -> Map FileName (DirTree a)
+ System.Directory.Contents: removeSibling :: DirTree a -> Map FileName (DirTree a) -> Map FileName (DirTree a)
+ System.Directory.Contents: type FileName = String
+ System.Directory.Contents: withFirstChild :: Map FileName (DirTree a) -> (DirTree a -> Map FileName (DirTree a) -> x) -> Maybe x
+ System.Directory.Contents.Types: DirTree_Dir :: FilePath -> Map FileName (DirTree a) -> DirTree a
+ System.Directory.Contents.Types: DirTree_File :: FilePath -> a -> DirTree a
+ System.Directory.Contents.Types: DirTree_Symlink :: FilePath -> Symlink a -> DirTree a
+ System.Directory.Contents.Types: Symlink_External :: String -> Map FileName (DirTree a) -> Symlink a
+ System.Directory.Contents.Types: Symlink_Internal :: String -> FilePath -> Symlink a
+ System.Directory.Contents.Types: data DirTree a
+ System.Directory.Contents.Types: data Symlink a
+ System.Directory.Contents.Types: fileName :: DirTree a -> FileName
+ System.Directory.Contents.Types: fileNameMap :: [DirTree a] -> Map FileName (DirTree a)
+ System.Directory.Contents.Types: filePath :: DirTree a -> FilePath
+ System.Directory.Contents.Types: insertSibling :: DirTree a -> Map FileName (DirTree a) -> Map FileName (DirTree a)
+ System.Directory.Contents.Types: instance Data.Data.Data a => Data.Data.Data (System.Directory.Contents.Types.DirTree a)
+ System.Directory.Contents.Types: instance Data.Data.Data a => Data.Data.Data (System.Directory.Contents.Types.Symlink a)
+ System.Directory.Contents.Types: instance Data.Foldable.Foldable System.Directory.Contents.Types.DirTree
+ System.Directory.Contents.Types: instance Data.Foldable.Foldable System.Directory.Contents.Types.Symlink
+ System.Directory.Contents.Types: instance Data.Traversable.Traversable System.Directory.Contents.Types.DirTree
+ System.Directory.Contents.Types: instance Data.Traversable.Traversable System.Directory.Contents.Types.Symlink
+ System.Directory.Contents.Types: instance GHC.Base.Functor System.Directory.Contents.Types.DirTree
+ System.Directory.Contents.Types: instance GHC.Base.Functor System.Directory.Contents.Types.Symlink
+ System.Directory.Contents.Types: instance GHC.Classes.Eq a => GHC.Classes.Eq (System.Directory.Contents.Types.DirTree a)
+ System.Directory.Contents.Types: instance GHC.Classes.Eq a => GHC.Classes.Eq (System.Directory.Contents.Types.Symlink a)
+ System.Directory.Contents.Types: instance GHC.Classes.Ord a => GHC.Classes.Ord (System.Directory.Contents.Types.DirTree a)
+ System.Directory.Contents.Types: instance GHC.Classes.Ord a => GHC.Classes.Ord (System.Directory.Contents.Types.Symlink a)
+ System.Directory.Contents.Types: instance GHC.Generics.Generic (System.Directory.Contents.Types.DirTree a)
+ System.Directory.Contents.Types: instance GHC.Generics.Generic (System.Directory.Contents.Types.Symlink a)
+ System.Directory.Contents.Types: instance GHC.Read.Read a => GHC.Read.Read (System.Directory.Contents.Types.DirTree a)
+ System.Directory.Contents.Types: instance GHC.Read.Read a => GHC.Read.Read (System.Directory.Contents.Types.Symlink a)
+ System.Directory.Contents.Types: instance GHC.Show.Show a => GHC.Show.Show (System.Directory.Contents.Types.DirTree a)
+ System.Directory.Contents.Types: instance GHC.Show.Show a => GHC.Show.Show (System.Directory.Contents.Types.Symlink a)
+ System.Directory.Contents.Types: removeSibling :: DirTree a -> Map FileName (DirTree a) -> Map FileName (DirTree a)
+ System.Directory.Contents.Types: type FileName = String
+ System.Directory.Contents.Types: withFirstChild :: Map FileName (DirTree a) -> (DirTree a -> Map FileName (DirTree a) -> x) -> Maybe x
+ System.Directory.Contents.Zipper: DirZipper :: DirTree a -> Map FilePath (DirTree a) -> [(DirTree a, Map FilePath (DirTree a))] -> DirZipper a
+ System.Directory.Contents.Zipper: NavLeft :: NavSibling
+ System.Directory.Contents.Zipper: NavRight :: NavSibling
+ System.Directory.Contents.Zipper: [_dirZipper_cursor] :: DirZipper a -> DirTree a
+ System.Directory.Contents.Zipper: [_dirZipper_elders] :: DirZipper a -> [(DirTree a, Map FilePath (DirTree a))]
+ System.Directory.Contents.Zipper: [_dirZipper_siblings] :: DirZipper a -> Map FilePath (DirTree a)
+ System.Directory.Contents.Zipper: data DirZipper a
+ System.Directory.Contents.Zipper: data NavSibling
+ System.Directory.Contents.Zipper: down :: DirZipper a -> Maybe (DirZipper a)
+ System.Directory.Contents.Zipper: downTo :: FileName -> DirZipper a -> Maybe (DirZipper a)
+ System.Directory.Contents.Zipper: focused :: DirZipper a -> DirTree a
+ System.Directory.Contents.Zipper: followLink :: DirZipper a -> Maybe (DirZipper a)
+ System.Directory.Contents.Zipper: followRelative :: FilePath -> DirZipper a -> Maybe (DirZipper a)
+ System.Directory.Contents.Zipper: home :: DirZipper a -> DirZipper a
+ System.Directory.Contents.Zipper: insert :: DirTree a -> DirZipper a -> DirZipper a
+ System.Directory.Contents.Zipper: instance GHC.Classes.Eq a => GHC.Classes.Eq (System.Directory.Contents.Zipper.DirZipper a)
+ System.Directory.Contents.Zipper: instance GHC.Classes.Ord a => GHC.Classes.Ord (System.Directory.Contents.Zipper.DirZipper a)
+ System.Directory.Contents.Zipper: instance GHC.Read.Read a => GHC.Read.Read (System.Directory.Contents.Zipper.DirZipper a)
+ System.Directory.Contents.Zipper: instance GHC.Show.Show a => GHC.Show.Show (System.Directory.Contents.Zipper.DirZipper a)
+ System.Directory.Contents.Zipper: left :: DirZipper a -> Maybe (DirZipper a)
+ System.Directory.Contents.Zipper: mapCursor :: (DirTree a -> DirTree a) -> DirZipper a -> DirZipper a
+ System.Directory.Contents.Zipper: nextSibling :: NavSibling -> DirZipper a -> Maybe (DirZipper a)
+ System.Directory.Contents.Zipper: remove :: DirZipper a -> Maybe (DirZipper a)
+ System.Directory.Contents.Zipper: replaceCursor :: DirTree a -> DirZipper a -> DirZipper a
+ System.Directory.Contents.Zipper: right :: DirZipper a -> Maybe (DirZipper a)
+ System.Directory.Contents.Zipper: toSibling :: FileName -> DirZipper a -> Maybe (DirZipper a)
+ System.Directory.Contents.Zipper: unzipped :: DirZipper a -> DirTree a
+ System.Directory.Contents.Zipper: up :: DirZipper a -> Maybe (DirZipper a)
+ System.Directory.Contents.Zipper: zipped :: DirTree a -> DirZipper a
- System.Directory.Contents: DirTree_Dir :: FilePath -> [DirTree a] -> DirTree a
+ System.Directory.Contents: DirTree_Dir :: FilePath -> Map FileName (DirTree a) -> DirTree a
- System.Directory.Contents: Symlink_External :: String -> [DirTree a] -> Symlink a
+ System.Directory.Contents: Symlink_External :: String -> Map FileName (DirTree a) -> Symlink a
Files
- CHANGELOG.md +7/−0
- README.lhs +48/−1
- README.md +48/−1
- directory-contents.cabal +18/−21
- src/System/Directory/Contents.hs +88/−74
- src/System/Directory/Contents/Types.hs +82/−0
- src/System/Directory/Contents/Zipper.hs +190/−0
- test/Test.hs +13/−1
CHANGELOG.md view
@@ -1,5 +1,12 @@ # Revision history for directory-contents +## 0.2.0.0++* Add `Data.Data` instance for `DirTree` and `Symlink`+* Add `System.Directory.Contents.Zipper` for convenient navigation and modification of directory contents+* Add more examples to readme+* Breaking change: `DirTree_Dir` and `Symlink_External` child nodes are now stored as a `Map`+ ## 0.1.0.0 * Build recursive directory trees and try to avoid symlink cycles. Includes functions to walk a directory tree, filter it, and display it.
README.lhs view
@@ -2,7 +2,8 @@ ================== [](https://haskell.org) [](https://hackage.haskell.org/package/directory-contents) [](https://matrix.hackage.haskell.org/#/package/directory-contents) [](https://github.com/obsidiansystems/directory-contents/actions) [](https://github.com/obsidiansystems/directory-contents/blob/master/LICENSE) -Recursively list the contents of a directory while avoiding symlink loops.+Recursively list, operate on, and navigate the contents of a directory while+avoiding symlink loops. Description -----------@@ -26,14 +27,36 @@ > import Data.List > import qualified Data.Text as T > import System.Directory.Contents+> import System.Directory.Contents.Zipper > import System.FilePath > > main :: IO () > main = do++```+Building a directory tree is easy. Just call `buildDirTree` on a path of your+choice. It'll recursively enumerate the contents of the directories. If it+encounters symlinks, it'll follow those symlinks if it hasn't yet encountered+the target of the symlink. If it has, it'll store a reference to that+already-seen target.++```haskell+ > mp <- buildDirTree "." > case mp of > Nothing -> putStrLn "Couldn't find that path." > Just p -> do++```+Once you've got a `DirTree` you can fmap, traverse, filter, or+[wither](https://hackage.haskell.org/package/witherable-class) it to transform+it however you like.++Note that the filtering operations generally do not remove empty directories.+You have to call `pruneDirTree` to do that.++```haskell+ > let f = pruneDirTree =<< filterDirTree ((`elem` [".hs", ".lhs"]) . takeExtension) p > putStrLn $ case f of > Nothing -> "No haskell source files found."@@ -44,6 +67,30 @@ > , "Haskell source files:" > , intercalate ", " $ F.toList hs > ]++```++You can also use the provided `DirZipper` to browse your directory hierarchy and make+changes wherever you like.++```haskell++> let printFocused = maybe+> (putStrLn "Couldn't find navigation target")+> (printDirTree . focused)+>+> putStrLn "Navigating down to src/System/Directory:"+> printFocused $+> downTo "Directory" =<< downTo "System" =<< downTo "src" (zipped p)+>+> putStrLn "Navigating using a path containing \"..\":"+> printFocused $+> followRelative "./src/../src/System/Directory" (zipped p)+>+> putStrLn "Removing the src/System directory. The src folder is now empty"+> putStrLn "(note that this doesn't change the actual files):"+> printFocused $+> remove =<< followRelative "./src/System" (zipped p) > ```
README.md view
@@ -2,7 +2,8 @@ ================== [](https://haskell.org) [](https://hackage.haskell.org/package/directory-contents) [](https://matrix.hackage.haskell.org/#/package/directory-contents) [](https://github.com/obsidiansystems/directory-contents/actions) [](https://github.com/obsidiansystems/directory-contents/blob/master/LICENSE) -Recursively list the contents of a directory while avoiding symlink loops.+Recursively list, operate on, and navigate the contents of a directory while+avoiding symlink loops. Description -----------@@ -26,14 +27,36 @@ > import Data.List > import qualified Data.Text as T > import System.Directory.Contents+> import System.Directory.Contents.Zipper > import System.FilePath > > main :: IO () > main = do++```+Building a directory tree is easy. Just call `buildDirTree` on a path of your+choice. It'll recursively enumerate the contents of the directories. If it+encounters symlinks, it'll follow those symlinks if it hasn't yet encountered+the target of the symlink. If it has, it'll store a reference to that+already-seen target.++```haskell+ > mp <- buildDirTree "." > case mp of > Nothing -> putStrLn "Couldn't find that path." > Just p -> do++```+Once you've got a `DirTree` you can fmap, traverse, filter, or+[wither](https://hackage.haskell.org/package/witherable-class) it to transform+it however you like.++Note that the filtering operations generally do not remove empty directories.+You have to call `pruneDirTree` to do that.++```haskell+ > let f = pruneDirTree =<< filterDirTree ((`elem` [".hs", ".lhs"]) . takeExtension) p > putStrLn $ case f of > Nothing -> "No haskell source files found."@@ -44,6 +67,30 @@ > , "Haskell source files:" > , intercalate ", " $ F.toList hs > ]++```++You can also use the provided `DirZipper` to browse your directory hierarchy and make+changes wherever you like.++```haskell++> let printFocused = maybe+> (putStrLn "Couldn't find navigation target")+> (printDirTree . focused)+>+> putStrLn "Navigating down to src/System/Directory:"+> printFocused $+> downTo "Directory" =<< downTo "System" =<< downTo "src" (zipped p)+>+> putStrLn "Navigating using a path containing \"..\":"+> printFocused $+> followRelative "./src/../src/System/Directory" (zipped p)+>+> putStrLn "Removing the src/System directory. The src folder is now empty"+> putStrLn "(note that this doesn't change the actual files):"+> printFocused $+> remove =<< followRelative "./src/System" (zipped p) > ```
directory-contents.cabal view
@@ -1,9 +1,9 @@ cabal-version: >=1.10 name: directory-contents-version: 0.1.0.0-synopsis: Recursively build a tree of directory contents.+version: 0.2.0.0+synopsis: Recursively build, navigate, and operate on a tree of directory contents. description:- Like the linux tree command, this library recursively constructs a tree of directory contents while detecting and avoiding symlink cycles.+ Like the linux tree command, this library recursively constructs a tree of directory contents while detecting and avoiding symlink cycles. It also provides functions for operating on and navigating the contents. bug-reports: https://github.com/obsidiansystems/directory-contents/issues@@ -21,14 +21,16 @@ library exposed-modules: System.Directory.Contents+ System.Directory.Contents.Types+ System.Directory.Contents.Zipper build-depends:- base >=4.12 && <4.15- , containers >=0.6 && <0.7- , directory >=1.3 && <1.4- , filepath >=1.4 && <1.5- , text >=1.2 && <1.3- , transformers >=0.5 && <0.6- , witherable >=0.3 && <0.4+ base >=4.12 && <4.15+ , containers >=0.6 && <0.7+ , directory >=1.3 && <1.4+ , filepath >=1.4 && <1.5+ , text >=1.2 && <1.3+ , transformers >=0.5 && <0.6+ , witherable >=0.3 && <0.4 hs-source-dirs: src default-language: Haskell2010@@ -40,21 +42,16 @@ , directory-contents , filepath , text-- main-is: README.lhs+ main-is: README.lhs default-language: Haskell2010- ghc-options: -Wall -optL -q+ ghc-options: -Wall -optL -q test-suite directory-contents-test- type: exitcode-stdio-1.0- build-depends:- base- , directory-contents- , filepath-+ type: exitcode-stdio-1.0+ build-depends: base, directory-contents, filepath default-language: Haskell2010- hs-source-dirs: test- main-is: Test.hs+ hs-source-dirs: test+ main-is: Test.hs source-repository head type: git
src/System/Directory/Contents.hs view
@@ -1,11 +1,9 @@-{-# language DeriveFoldable #-}-{-# language DeriveFunctor #-}-{-# language DeriveGeneric #-}-{-# language DeriveTraversable #-}-{-# language LambdaCase #-}-{-# language MultiWayIf #-}-{-# language ScopedTypeVariables #-}-+{-# Language DeriveFoldable #-}+{-# Language DeriveFunctor #-}+{-# Language DeriveTraversable #-}+{-# Language FlexibleContexts #-}+{-# Language LambdaCase #-}+{-# Language MultiWayIf #-} {-| Description: Recursively list the contents of a directory while avoiding@@ -18,8 +16,48 @@ In addition to building the directory-contents tree, this module provides facilities for filtering, displaying, and navigating the directory hierarchy. +See 'System.Directory.Contents.Zipper.DirZipper' for zipper-based navigation.+ -}-module System.Directory.Contents where+module System.Directory.Contents + (+ -- * Directory hierarchy tree+ DirTree(..)+ , Symlink(..)+ , FileName+ -- ** Constructing directory trees+ , buildDirTree+ , dereferenceSymlinks+ -- ** Lower level tree construction+ -- *** Extracting basic file information+ , filePath+ , fileName+ -- *** Building and manipulating a map of sibling files+ , fileNameMap+ , insertSibling+ , removeSibling+ , withFirstChild+ -- * Basic directory tree navigation+ , walkDirTree+ , walkContents+ -- * Filtering a directory tree+ , pruneDirTree+ , DirTreeMaybe(..)+ , withDirTreeMaybe+ , withDirTreeMaybeF+ , witherDirTree+ , filterADirTree+ , mapMaybeDirTree+ , catMaybesDirTree+ , filterDirTree+ -- * Displaying a directory tree+ , drawDirTree+ , drawDirTreeWith+ , printDirTree+ -- * Miscellaneous+ , mkRelative+ , alternative+ ) where import Control.Applicative import Control.Monad@@ -27,40 +65,20 @@ import Data.List import qualified Data.Map as Map import Data.Monoid+import Data.Set (Set)+import qualified Data.Set as Set import Data.Text (Text) import qualified Data.Text as T import Data.Tree as DataTree import Data.Witherable-import GHC.Generics import System.Directory import System.FilePath --- | The contents of a directory, represented as a tree. See 'Symlink' for--- special handling of symlinks.-data DirTree a- = DirTree_Dir FilePath [DirTree a]- | DirTree_File FilePath a- | DirTree_Symlink FilePath (Symlink a)- deriving (Show, Read, Eq, Ord, Functor, Foldable, Traversable, Generic)+import System.Directory.Contents.Types+import System.Directory.Contents.Zipper --- | Symlink cycles are prevented by separating symlinks into two categories:--- those that point to paths already within the directory hierarchy being--- recursively listed, and those that are not. In the former case, rather than--- following the symlink and listing the target redundantly, we simply store--- the symlink reference itself. In the latter case, we treat the symlink as we--- would any other folder and produce a list of its contents.------ The 'String' argument represents the symlink reference (e.g., "../somefile").--- In the 'Symlink_Internal' case, the second ('FilePath') argument is the path--- to the symlink target.--- In the 'Symlink_External' case, the second (@[DirTree a]@) argument contains--- the contents of the symlink target.-data Symlink a- = Symlink_Internal String FilePath- | Symlink_External String [DirTree a]- deriving (Show, Read, Eq, Ord, Functor, Foldable, Traversable, Generic)+-- * Construct --- * Constructing a tree -- | Recursively list the contents of a 'FilePath', representing the results as -- a hierarchical 'DirTree'. This function should produce results similar to -- the linux command @tree -l@.@@ -81,7 +99,7 @@ -- > └── C -> ../C -- > └── c ----- this function will produce the following (as rendererd by 'drawDirTree'):+-- this function will produce the following (as rendered by 'drawDirTree'): -- -- > test -- > |@@ -121,7 +139,7 @@ | isSym -> case Map.lookup canon seen' of Nothing -> do s <- getSymbolicLinkTarget path- Just . DirTree_Symlink path . Symlink_External s <$> buildSubpaths+ Just . DirTree_Symlink path . Symlink_External s . fileNameMap <$> buildSubpaths Just _ -> do target <- getSymbolicLinkTarget path canonRoot <- canonicalizePath root@@ -129,7 +147,7 @@ canonSym <- canonicalizePath $ takeDirectory path </> target pure $ Just $ DirTree_Symlink path $ Symlink_Internal target $ startingPoint </> mkRelative canonRoot canonSym- | isDir -> Just . DirTree_Dir path <$> buildSubpaths+ | isDir -> Just . DirTree_Dir path . fileNameMap <$> buildSubpaths | otherwise -> pure $ Just $ DirTree_File path path -- | De-reference one layer of symlinks@@ -174,13 +192,14 @@ DirTree_Dir p xs -> DirTree_Dir p <$> mapM (deref top) xs DirTree_File p x -> pure $ DirTree_File p x DirTree_Symlink p sym -> case sym of- Symlink_External _ paths -> case paths of- [] -> do- isDir <- doesDirectoryExist p- pure $ if isDir- then DirTree_Dir p []- else DirTree_File p p- ps -> pure $ DirTree_Dir p ps+ Symlink_External _ paths ->+ if Map.null paths+ then do+ isDir <- doesDirectoryExist p+ pure $ if isDir+ then DirTree_Dir p Map.empty+ else DirTree_File p p+ else pure $ DirTree_Dir p paths Symlink_Internal _ r -> do let startingPoint = takeFileName $ filePath top let target = walkDirTree (startingPoint </> r) top@@ -204,7 +223,8 @@ -- > `- Contents.hs -- -- This function does not dereference symlinks, nor does it handle the special--- paths @.@ and @..@.+-- paths @.@ and @..@. For more advanced navigation, including handling of special+-- paths, see 'System.Directory.Contents.Zipper.DirZipper'. walkDirTree :: FilePath -> DirTree a -> Maybe (DirTree a) walkDirTree target p = let pathSegments = splitDirectories target@@ -212,14 +232,14 @@ walk [] path = Just path walk (c : gc) path = case path of DirTree_Dir a xs- | takeFileName a == c -> alternative $ walk gc <$> xs+ | takeFileName a == c -> alternative $ walk gc <$> Map.elems xs DirTree_File a f | takeFileName a == c && null gc -> Just $ DirTree_File a f DirTree_Symlink a (Symlink_Internal s t) | takeFileName a == c && null gc -> Just $ DirTree_Symlink a (Symlink_Internal s t) DirTree_Symlink a (Symlink_External _ xs)- | takeFileName a == c -> alternative $ walk gc <$> xs+ | takeFileName a == c -> alternative $ walk gc <$> Map.elems xs _ -> Nothing in walk pathSegments p @@ -238,15 +258,11 @@ -- > Directory -- > | -- > `- Contents.hs+--+--For more advanced navigation, see+--'System.Directory.Contents.Zipper.DirZipper'. walkContents :: FilePath -> DirTree a -> Maybe (DirTree a)-walkContents p = \case- DirTree_Dir _ xs -> walkSub xs- DirTree_File _ _ -> Nothing- DirTree_Symlink _ (Symlink_External _ xs) -> walkSub xs- DirTree_Symlink _ (Symlink_Internal _ _) -> Nothing- where- walkSub :: [DirTree a] -> Maybe (DirTree a)- walkSub xs = getAlt $ mconcat $ Alt . walkDirTree p <$> xs+walkContents p = fmap focused . followRelative p . zipped -- * Filter -- | This wrapper really just represents the no-path/empty case so that@@ -257,28 +273,33 @@ instance Filterable DirTreeMaybe where catMaybes (DirTreeMaybe Nothing) = DirTreeMaybe Nothing catMaybes (DirTreeMaybe (Just x)) = DirTreeMaybe $ do- let go :: DirTree (Maybe a) -> Writer [FilePath] (Maybe (DirTree a))+ let go :: DirTree (Maybe a) -> Writer (Set FilePath) (Maybe (DirTree a)) go = \case DirTree_Dir p xs -> do out <- mapM go xs pure $ Just $ DirTree_Dir p $ catMaybes out DirTree_File p f -> case f of- Nothing -> tell [p] >> pure Nothing+ Nothing -> tell (Set.singleton p) >> pure Nothing Just f' -> pure $ Just $ DirTree_File p f' DirTree_Symlink p (Symlink_External s xs) -> do out <- mapM go xs pure $ Just $ DirTree_Symlink p (Symlink_External s $ catMaybes out) DirTree_Symlink p (Symlink_Internal s r) -> pure $ Just $ DirTree_Symlink p (Symlink_Internal s r)- removeStaleSymlinks :: [FilePath] -> DirTree a -> Maybe (DirTree a)- removeStaleSymlinks xs = \case+ removeStaleSymlinks :: Set FilePath -> DirTree a -> Maybe (DirTree a)+ removeStaleSymlinks xs d = case d of DirTree_Symlink p (Symlink_Internal s r) ->- let startingPoint = takeDirectory $ filePath x+ let startingPoint = case takeDirectory $ filePath x of+ "." -> ""+ a -> a in- if (startingPoint </> r) `elem` xs+ if (startingPoint </> r) `Set.member` xs then Nothing else Just $ DirTree_Symlink p (Symlink_Internal s r)- DirTree_Symlink p s -> Just $ DirTree_Symlink p s+ DirTree_Symlink p (Symlink_External s cs) ->+ if Map.null cs && Set.member (filePath x </> s) xs+ then Nothing+ else Just $ DirTree_Symlink p (Symlink_External s cs) DirTree_File p f -> Just $ DirTree_File p f DirTree_Dir p fs -> Just $ DirTree_Dir p $ catMaybes $ removeStaleSymlinks xs <$> fs@@ -346,9 +367,9 @@ DirTree_Symlink a (Symlink_Internal s t) -> Just $ DirTree_Symlink a (Symlink_Internal s t) where- sub c xs = case mapMaybe pruneDirTree xs of- [] -> Nothing- ys -> Just $ c ys+ sub c xs =+ let ys = mapMaybe pruneDirTree xs+ in if Map.null ys then Nothing else Just $ c ys -- * Display -- | Produces a tree drawing (using only text) of a 'DirTree' hierarchy.@@ -363,11 +384,11 @@ DirTree_File p a -> DataTree.Node (f (takeFileName p) a) [] DirTree_Dir p ps ->- DataTree.Node (takeFileName p) $ pathToTree <$> ps+ DataTree.Node (takeFileName p) $ pathToTree <$> Map.elems ps DirTree_Symlink p (Symlink_Internal s _) -> DataTree.Node (showSym p s) [] DirTree_Symlink p (Symlink_External s xs) ->- DataTree.Node (showSym p s) $ pathToTree <$> xs+ DataTree.Node (showSym p s) $ pathToTree <$> Map.elems xs showSym p s = takeFileName p <> " -> " <> s -- | Print the 'DirTree' as a tree. For example:@@ -398,10 +419,3 @@ -- | Get the first 'Alternative' alternative :: Alternative f => [f a] -> f a alternative = getAlt . mconcat . fmap Alt---- | Extract the 'FilePath' from a 'DirTree' node-filePath :: DirTree a -> FilePath-filePath = \case- DirTree_Dir f _ -> f- DirTree_File f _ -> f- DirTree_Symlink f _ -> f
+ src/System/Directory/Contents/Types.hs view
@@ -0,0 +1,82 @@+{-# language DeriveDataTypeable #-}+{-# language DeriveFoldable #-}+{-# language DeriveFunctor #-}+{-# language DeriveGeneric #-}+{-# language DeriveTraversable #-}+{-# language LambdaCase #-}+{-|+Description:+ Contains the tree data structure used to store directory hierarchies+-}+module System.Directory.Contents.Types where++import Data.Data+import Data.Map (Map)+import qualified Data.Map as Map+import GHC.Generics+import System.FilePath++-- | The contents of a directory, represented as a tree. See 'Symlink' for+-- special handling of symlinks.+data DirTree a+ = DirTree_Dir FilePath (Map FileName (DirTree a))+ | DirTree_File FilePath a+ | DirTree_Symlink FilePath (Symlink a)+ deriving (Show, Read, Eq, Ord, Functor, Foldable, Traversable, Generic, Data)++-- | Symlink cycles are prevented by separating symlinks into two categories:+-- those that point to paths already within the directory hierarchy being+-- recursively listed, and those that are not. In the former case, rather than+-- following the symlink and listing the target redundantly, we simply store+-- the symlink reference itself. In the latter case, we treat the symlink as we+-- would any other folder and produce a list of its contents.+--+-- The 'String' argument represents the symlink reference (e.g., "../somefile").+-- In the 'Symlink_Internal' case, the second ('FilePath') argument is the path+-- to the symlink target.+-- In the 'Symlink_External' case, the second (@[DirTree a]@) argument contains+-- the contents of the symlink target.+data Symlink a+ = Symlink_Internal String FilePath+ | Symlink_External String (Map FileName (DirTree a))+ deriving (Show, Read, Eq, Ord, Functor, Foldable, Traversable, Generic, Data)++-- * Utilities++-- | Extract the 'FilePath' from a 'DirTree' node+filePath :: DirTree a -> FilePath+filePath = \case+ DirTree_Dir f _ -> f+ DirTree_File f _ -> f+ DirTree_Symlink f _ -> f++-- | File names, as opposed to file paths, are used to uniquely identify+-- siblings at each level+type FileName = String++-- | Generate the key used to identify siblings+fileName :: DirTree a -> FileName+fileName = takeFileName . filePath++-- | Construct a map of files indexed by filename. Should only be used for a+-- particular generation or level in the directory hierarchy (since that's the+-- only time we can be sure that names are unique)+fileNameMap :: [DirTree a] -> Map FileName (DirTree a)+fileNameMap xs = Map.fromList $ zip (fileName <$> xs) xs++-- | Add a sibling to a map of files+insertSibling :: DirTree a -> Map FileName (DirTree a) -> Map FileName (DirTree a)+insertSibling a = Map.insert (fileName a) a++-- | Remove sibling from a map of files+removeSibling :: DirTree a -> Map FileName (DirTree a) -> Map FileName (DirTree a)+removeSibling a = Map.delete (fileName a)++-- | Map a function over the first child and the rest of the children+withFirstChild+ :: Map FileName (DirTree a)+ -> (DirTree a -> Map FileName (DirTree a) -> x)+ -> Maybe x+withFirstChild m f = case Map.minView m of+ Nothing -> Nothing+ Just (firstChild, children) -> Just $ f firstChild children
+ src/System/Directory/Contents/Zipper.hs view
@@ -0,0 +1,190 @@+{-# Language LambdaCase #-}+{-|+Description:+ Cursor-based navigation and modification of 'DirTree's.++This module should be imported qualified due to the very short names it exports.+-}+module System.Directory.Contents.Zipper where++import Control.Applicative+import Data.Map (Map)+import qualified Data.Map as Map+import System.FilePath++import System.Directory.Contents.Types++-- * Zipper++-- | A zipper for a 'DirTree'. As you navigate the tree, this keeps track of where+-- you are (which node is "focused" under your cursor) and how to reach parent, child,+-- and sibling nodes.+data DirZipper a = DirZipper+ { _dirZipper_cursor :: DirTree a -- ^ Cursor position+ , _dirZipper_siblings :: Map FilePath (DirTree a) -- ^ Siblings+ , _dirZipper_elders :: [(DirTree a, Map FilePath (DirTree a))]+ -- ^ Parents and aunts/uncles, in reverse order (i.e., immediate ancestors first)+ }+ deriving (Show, Read, Eq, Ord)++-- | Construct a zipper out of a 'DirTree'. Use 'focused' or 'unzipped' to get back+-- a 'DirTree'+zipped :: DirTree a -> DirZipper a+zipped a = DirZipper a Map.empty []++-- | The currently focused/selected node (and its children). In other words,+-- where you are in the directory hierarchy.+focused :: DirZipper a -> DirTree a+focused = _dirZipper_cursor++-- | Throws away your current cursor information and returns the entire 'DirTree'+-- contained by the 'DirZipper'.+--+-- > unzipped . zipped == id+--+unzipped :: DirZipper a -> DirTree a+unzipped = focused . home++-- | Move down a level in the directory hierarchy. To move down to a specific child,+-- use 'downTo'.+down :: DirZipper a -> Maybe (DirZipper a)+down dz = case dz of+ DirZipper p@(DirTree_Dir _ xs) siblings parents ->+ withFirstChild xs $ \firstChild children ->+ DirZipper firstChild children $ (p, siblings) : parents+ DirZipper p@(DirTree_Symlink _ (Symlink_External _ xs)) siblings parents ->+ withFirstChild xs $ \firstChild children ->+ DirZipper firstChild children $ (p, siblings) : parents+ DirZipper (DirTree_Symlink _ (Symlink_Internal _ ref)) _ _ ->+ followRelative ref $ home dz+ _ -> Nothing++-- | Move up a level in the directory hierarchy, back to the parent that you+-- previously moved 'down' through.+up :: DirZipper a -> Maybe (DirZipper a)+up = \case+ DirZipper c s ((parent, uncles):ps) ->+ Just $ DirZipper (update c s parent) uncles ps+ _ -> Nothing+ where+ update :: DirTree a -> Map FilePath (DirTree a) -> DirTree a -> DirTree a+ update child siblings parent = case parent of+ DirTree_Dir f _ -> DirTree_Dir f $ insertSibling child siblings+ DirTree_Symlink f (Symlink_External s _) ->+ DirTree_Symlink f $ Symlink_External s $ insertSibling child siblings+ _ -> parent++-- | Go to the top of the directory hierarchy.+home :: DirZipper a -> DirZipper a+home dz =+ let upmost z = maybe z upmost $ up z+ in upmost dz++-- | Navigation directions for sibling nodes+data NavSibling = NavLeft | NavRight++-- | Move to the sibling next to the focused node+nextSibling :: NavSibling -> DirZipper a -> Maybe (DirZipper a)+nextSibling nav (DirZipper cursor siblings parents) =+ let kids = insertSibling cursor siblings+ next = case nav of+ NavRight -> Map.lookupGT (fileName cursor) kids+ NavLeft -> Map.lookupLT (fileName cursor) kids+ in case next of+ Nothing -> Nothing+ Just (_, sibling) -> Just $+ DirZipper sibling (removeSibling sibling kids) parents++-- | Move to the sibling to the left of the focused node+left :: DirZipper a -> Maybe (DirZipper a)+left = nextSibling NavLeft++-- | Move to the sibling to the right of the focused node+right :: DirZipper a -> Maybe (DirZipper a)+right = nextSibling NavRight++-- | Go to a particular sibling+toSibling :: FileName -> DirZipper a -> Maybe (DirZipper a)+toSibling name (DirZipper cursor siblings parents) =+ case Map.lookup name siblings of+ Nothing -> Nothing+ Just sibling ->+ let otherSiblings = insertSibling cursor $+ removeSibling sibling siblings+ in Just $ DirZipper sibling otherSiblings parents++-- | Move down in the directory hierarchy to a particular child+downTo :: FileName -> DirZipper a -> Maybe (DirZipper a)+downTo name z = do+ d <- down z+ if fileName (focused d) == name+ then pure d+ else toSibling name d++-- | Modify the focused node+mapCursor+ :: (DirTree a -> DirTree a)+ -> DirZipper a+ -> DirZipper a+mapCursor f (DirZipper cursor siblings parents) =+ DirZipper (f cursor) siblings parents++-- | Replace the focused node+replaceCursor+ :: DirTree a+ -> DirZipper a+ -> DirZipper a+replaceCursor = mapCursor . const++-- | Add a new sibling to the focused node's generation and focus on it+insert+ :: DirTree a+ -> DirZipper a+ -> DirZipper a+insert d (DirZipper cursor siblings parents) =+ DirZipper+ d+ (insertSibling cursor siblings)+ parents++-- | Remove the focused node+remove+ :: DirZipper a+ -> Maybe (DirZipper a)+remove z@(DirZipper cursor _ _) =+ let rm (DirZipper c s p) =+ DirZipper c (removeSibling cursor s) p+ in case rm <$> (left z <|> right z) of+ Just s -> Just s+ Nothing -> case up z of+ Nothing -> Nothing+ Just dz -> Just $ flip replaceCursor dz $+ case _dirZipper_cursor dz of+ DirTree_Dir f _ -> DirTree_Dir f Map.empty+ DirTree_Symlink f (Symlink_External s _) ->+ DirTree_Symlink f (Symlink_External s Map.empty)+ x -> x++-- | Try to navigate the provided (possibly relative) path.+followRelative+ :: FilePath+ -> DirZipper a+ -> Maybe (DirZipper a)+followRelative path dz =+ let follow r z = case r of+ "." -> Just z+ ".." -> up z+ _ -> downTo r z <|> toSibling r z+ go rs z = case rs of+ [] -> Just z+ (r:more) -> go more =<< follow r z+ in go (splitDirectories path) dz++-- | If the focused node is an internal symlink (see 'Symlink'), try to get+-- to the target.+followLink+ :: DirZipper a+ -> Maybe (DirZipper a)+followLink z = case z of+ DirZipper (DirTree_Symlink _ (Symlink_Internal s _)) _ _ -> followRelative s z+ _ -> Nothing
test/Test.hs view
@@ -1,5 +1,6 @@ import Data.Maybe import System.Directory.Contents+import System.Directory.Contents.Zipper import System.Exit import System.FilePath @@ -19,13 +20,21 @@ pure $ expectNothing $ walkContents "C" =<< pruneDirTree filtered rocket <- test "Check that symlink can be traversed" $ pure (walkContents "D/rocket" filtered)+ test "Check that filtering removes internal symlinks to filtered files" $+ pure $ expectNothing $+ walkContents "C/info.md" filtered let filterRockets = filterDirTree ((/="rocket") . takeFileName)- test "Check that filtering removes symlinks to filtered files" $+ test "Check that filtering removes external symlinks to filtered files" $ pure $ expectNothing $ walkContents "d_rocket" =<< filterRockets p test "Check that dereferenced symlinks to filtered files are not removed" $ do deref <- dereferenceSymlinks p pure $ walkContents "d_rocket" =<< filterRockets deref+ test "Zipper down then up from root == id" $+ pure $ expectTrue $ fmap focused (up =<< down (zipped p)) == Just p+ test "Use zipper to remove a node" $ do+ let a = up =<< remove =<< downTo "info.md" =<< downTo "C" (zipped p)+ pure $ expectNothing $ walkContents "C/info.md" . focused =<< a readFile (filePath rocket) >>= putStr >> putStrLn " All systems go!" @@ -42,3 +51,6 @@ expectNothing x = case x of Nothing -> Just () Just _ -> Nothing++expectTrue :: Bool -> Maybe ()+expectTrue x = if x then Just () else Nothing