packages feed

unix-recursive (empty) → 0.1.0.0

raw patch · 16 files changed

+1675/−0 lines, 16 filesdep +basedep +bytestringdep +criterionsetup-changed

Dependencies added: base, bytestring, criterion, dir-traverse, directory, dirstream, hspec, pipes, pipes-safe, system-filepath, unix, unix-recursive, utf8-string

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for watch-fs-hs++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Author name here (c) 2021++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Author name here nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,57 @@+# Unix Recursive++![build](https://github.com/turboMaCk/unix-recursive/workflows/Test/badge.svg?branch=main&event=push)++Blazingly fast functions for recursive file system operations.+Utilizing lazy IO for constant space & computation efficiant bindigns to Posix [`dirstream.h`](https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/unix/dirstream.h;h=8303f07fab6f6efaa39e51411ef924e712d995e0;hb=fa39685d5c7df2502213418bead44e9543a9b9ec) api.++## Comparision with other libraries:++Tests were performed on relatively modern consumer grade HW with relatively modern consumer grade M.2 SSD drive.+All done on binaries compiled with optimization that are listing the same directory+with 1,705,184 items.++### [this lib] Unix Recursive `RawFilePath`/ByteString++![](docs/unix-recursive-bytestring.png)++- [source](bin/unix-recursive-bytestring.hs)++### [this lib] Unix Recursive `FilePath`/String++![](docs/unix-recursive-string.png)++- [source](bin/unix-recursive-string.hs)++### [alternative] Dir Traverse (`FilePath`/String is the only option)++![](docs/dir-traverse.png)++- [source](bin/dir-traverse.hs)+- [lib](https://hackage.haskell.org/package/dir-traverse)++### [alternative] Dirstream (`Filesystem.Path` is the only option)++![](docs/dirstream.png)++- [source](bin/dirstream.hs)+- [lib](https://hackage.haskell.org/package/dirstream)++## Testing performance++Cabal flag `bin` is being used for building the example binaries that can be used for measurement.++```+make bin+```++or to build and run one of the binaries:++```+$ stack build --flag unix-recursive:bin --exec "unix-recursive-bytestring-bin ${path-to-directory}"+```++## Hacking++This project uses the "Perfect Haskell Preprocessor" (PHP) for generating haskell source from meta module hs.+See [`gen`](gen) directory and [`MakeFile`](MakeFile) before you start hackking on stuff.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ System/Posix/Recursive.hs view
@@ -0,0 +1,274 @@+{- |+ Module      : System.Posix.Recursive-- Copyright   : (c) Marek Fajkus+ License     : BSD3++ Maintainer  : marek.faj@gmail.com++ All modules profided by @unix-recursive@ expose similar API.+ Make sure you're using the module wich best fits your needs based on:+   - Working  with 'RawFilePath' (faster and more packed) or 'FilePath' (slower but easier to work with safely)+   - Exception free (Default) or @Unsafe@ variants of functions++ = Usage++ This module is designed to be imported as @qualified@:++ > import qualified System.Posix.Recursive as PosixRecursive++ __Results__++ All functions return will return root path (the one they accept in argument) as a first item in the list:++ > head <$> PosixRecursive.list "System"+ > >>> "System"++ Other than that, this library __provides no guarantees about the order in which files appear in the resulting list__+ to make it possible to change the underlaying strategy in future versions.++ __Laziness__++ All IO operations are __guaranteed to be lazy and have constanct space characteristic__.+ Only the IO required by lazy computation will be performed so for instance running code like:++ > take 20 <$> PosixRecursive.listDirectories "/"++ Will perform only minimal ammount of IO needed to collect 20 directories on a root partition+-}+module System.Posix.Recursive (+    -- * Basic Listing+    -- $basic_listing+    list,+    followList,+    listMatching,+    followListMatching,++    -- * File Type Based Listing+    -- $type_based+    listAccessible,+    listDirectories,+    listRegularFiles,+    listSymbolicLinks,++    -- * Custom Listing+    -- $custom+    Conf (..),+    defConf,+    listCustom,+) where++import Control.Exception (bracket, handle)+import Data.Foldable (fold)+import System.IO.Unsafe (unsafeInterleaveIO)+import System.Posix.Files (FileStatus)++import qualified System.Posix.Directory as Posix+import qualified System.Posix.Files as Posix+++-- Helpers++foldMapA :: (Monoid b, Traversable t, Applicative f) => (a -> f b) -> t a -> f b+foldMapA = (fmap fold .) . traverse+++{-# INLINE listDir #-}+listDir :: (FilePath -> Bool) -> FilePath -> IO [FilePath]+listDir predicate path =+    bracket+        (Posix.openDirStream path)+        Posix.closeDirStream+        (go [])+  where+    go :: [FilePath] -> Posix.DirStream -> IO [FilePath]+    go acc dirp = do+        e <- Posix.readDirStream dirp+        if null e+            then pure acc+            else+                if e /= "." && e /= ".."+                    then+                        let fullPath = path <> "/" <> e+                         in if predicate fullPath+                                then go (fullPath : acc) dirp+                                else go acc dirp+                    else go acc dirp+{- $basic_listing+ Functions for listing contents of directory recursively.+ These functions list all the content they encounter while traversing+ the file system tree including directories, files, symlinks, broken symlinks.++  __Directories (and files) which process can't open due to permissions are listed but not recursed into.__++ Functions from this section is gurantee to always return the root path as a first element even+ if this path does not exist.++ > PosixRecursive.list "i-dont-exist"+ > >>> ["i-dont-exist"]++ In these cases the root path is considered the same way as symlink+ to non existing location.+-}+++{-# INLINE listAll' #-}+listAll' :: Bool -> (FilePath -> Bool) -> FilePath -> IO [FilePath]+listAll' followSymlinks predicate path =+    handle (\(_ :: IOError) -> pure []) $ do+        file <- getFileStatus path+        if Posix.isDirectory file+            then do+                content <- listDir predicate path++                next <- unsafeInterleaveIO $ foldMapA (listAll' followSymlinks predicate) content+                pure $ content ++ next+            else pure []+  where+    {-# INLINE getFileStatus #-}+    getFileStatus+        | followSymlinks = Posix.getFileStatus+        | otherwise = Posix.getSymbolicLinkStatus+++{-# INLINE listAll'' #-}+listAll'' :: Bool -> (FilePath -> Bool) -> FilePath -> IO [FilePath]+listAll'' followSymlinks predicate path =+    (path :) <$> listAll' followSymlinks predicate path+++-- | Like 'list' but uses provided function to test in which 'FilePath' to recurse into.+listMatching :: (FilePath -> Bool) -> FilePath -> IO [FilePath]+listMatching =+    listAll'' False+++-- | Like 'followList' but uses provided function to test in which 'FilePath' to recurse into.+followListMatching :: (FilePath -> Bool) -> FilePath -> IO [FilePath]+followListMatching =+    listAll'' True+++{- | List all files, directories & symlinks recursively.+ Symlinks are not followed. See 'followList'.+-}+list :: FilePath -> IO [FilePath]+list =+    listAll'' False (const True)+++{- | List all files, directories & symlinks recursively.+ Unlike 'list', symlinks are followed recursively as well.+-}+followList :: FilePath -> IO [FilePath]+followList =+    listAll'' True (const True)+++{- $custom+ All /File Type Based Listing/ functions are based on top of this interface.+ This part of API exposes exposes access for writing custom filtering functions.++ All paths are tested for filter functions so unreadble files won't appear in the result list:++ > PosixRecursive.listCustom PosixRecursive.defConf "i-dont-exist"+ > >>> []++ It's not possible to turn of this behaviour because this functions must get the 'FileStatus'+ type which requires reading each entry.+-}+++-- | Configuration arguments for 'listCustom'.+data Conf = Conf+    { -- | Filter paths algorithm should recurse into+      filterPath :: !(FilePath -> Bool)+    , -- | Test if file should be included in returned List+      includeFile :: !(FileStatus -> FilePath -> IO Bool)+    , -- | Follow symbolic links+      followSymlinks :: !Bool+    }+++{- | Default 'Conf'iguration.++> listCustom defConf == listAccessible++* Recurses into all Paths+* Includes all file types+* Does __not__ follow symlinks+-}+defConf :: Conf+defConf =+    Conf+        { filterPath = const True+        , includeFile = \_ _ -> pure True+        , followSymlinks = False+        }+++{-# INLINE listAccessible' #-}+listAccessible' :: Conf -> FilePath -> IO [FilePath]+listAccessible' Conf{..} path =+    handle (\(_ :: IOError) -> pure []) $ do+        file <- getFileStatus path+        next <-+            if Posix.isDirectory file+                then do+                    content <- listDir filterPath path+                    unsafeInterleaveIO $ foldMapA (listAccessible' Conf{..}) content+                else pure []++        include <- includeFile file path+        if include+            then pure $ path : next+            else pure next+  where+    {-# INLINE getFileStatus #-}+    getFileStatus+        | followSymlinks = Posix.getFileStatus+        | otherwise = Posix.getSymbolicLinkStatus+++-- | Recursively list files using custom filters.+listCustom :: Conf -> FilePath -> IO [FilePath]+listCustom =+    listAccessible'+++{- | Like 'list' but automatically filters out inacessible files like broken symlins+or unreadable files and directories.+-}+listAccessible :: FilePath -> IO [FilePath]+listAccessible =+    listAccessible' defConf+++{- $type_based+ Functions for listing specific file type. Reading the file type requires+ ability to read the file.++  __These function do not return broken symlinks and inacessible files__++ Include test is applied even for the root entry (path past in as an argument).+ This means that non existing paths are filtered.++ > PosixRecursive.listDirectories "i-dont-exist"+ > >>> []+-}+++-- | List sub directories of given directory.+listDirectories :: FilePath -> IO [FilePath]+listDirectories =+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isDirectory file}+++-- | Lists only files (while recursing into directories still).+listRegularFiles :: FilePath -> IO [FilePath]+listRegularFiles =+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isRegularFile file}+++-- | Lists only symbolic links (while recursing into directories still).+listSymbolicLinks :: FilePath -> IO [FilePath]+listSymbolicLinks =+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isSymbolicLink file}
+ System/Posix/Recursive/ByteString.hs view
@@ -0,0 +1,277 @@+{- |+ Module      : System.Posix.Recursive.ByteString-- Copyright   : (c) Marek Fajkus+ License     : BSD3++ Maintainer  : marek.faj@gmail.com++ All modules profided by @unix-recursive@ expose similar API.+ Make sure you're using the module wich best fits your needs based on:+   - Working  with 'RawFilePath' (faster and more packed) or 'FilePath' (slower but easier to work with safely)+   - Exception free (Default) or @Unsafe@ variants of functions++ = Usage++ This module is designed to be imported as @qualified@:++ > import qualified System.Posix.Recursive.ByteString as PosixRecursive++ __Results__++ All functions return will return root path (the one they accept in argument) as a first item in the list:++ > head <$> PosixRecursive.list "System"+ > >>> "System"++ Other than that, this library __provides no guarantees about the order in which files appear in the resulting list__+ to make it possible to change the underlaying strategy in future versions.++ __Laziness__++ All IO operations are __guaranteed to be lazy and have constanct space characteristic__.+ Only the IO required by lazy computation will be performed so for instance running code like:++ > take 20 <$> PosixRecursive.listDirectories "/"++ Will perform only minimal ammount of IO needed to collect 20 directories on a root partition+-}+module System.Posix.Recursive.ByteString (+    -- * Basic Listing+    -- $basic_listing+    list,+    followList,+    listMatching,+    followListMatching,++    -- * File Type Based Listing+    -- $type_based+    listAccessible,+    listDirectories,+    listRegularFiles,+    listSymbolicLinks,++    -- * Custom Listing+    -- $custom+    Conf (..),+    defConf,+    listCustom,+) where++import Control.Exception (bracket, handle)+import Data.Foldable (fold)+import System.IO.Unsafe (unsafeInterleaveIO)+import System.Posix.Files (FileStatus)++import qualified Data.ByteString as BS+import System.Posix.ByteString.FilePath (RawFilePath)++import qualified System.Posix.Directory.ByteString as Posix+import qualified System.Posix.Files.ByteString as Posix+++-- Helpers++foldMapA :: (Monoid b, Traversable t, Applicative f) => (a -> f b) -> t a -> f b+foldMapA = (fmap fold .) . traverse+++{-# INLINE listDir #-}+listDir :: (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]+listDir predicate path =+    bracket+        (Posix.openDirStream path)+        Posix.closeDirStream+        (go [])+  where+    go :: [RawFilePath] -> Posix.DirStream -> IO [RawFilePath]+    go acc dirp = do+        e <- Posix.readDirStream dirp+        if BS.null e+            then pure acc+            else+                if e /= "." && e /= ".."+                    then+                        let fullPath = path <> "/" <> e+                         in if predicate fullPath+                                then go (fullPath : acc) dirp+                                else go acc dirp+                    else go acc dirp+{- $basic_listing+ Functions for listing contents of directory recursively.+ These functions list all the content they encounter while traversing+ the file system tree including directories, files, symlinks, broken symlinks.++  __Directories (and files) which process can't open due to permissions are listed but not recursed into.__++ Functions from this section is gurantee to always return the root path as a first element even+ if this path does not exist.++ > PosixRecursive.list "i-dont-exist"+ > >>> ["i-dont-exist"]++ In these cases the root path is considered the same way as symlink+ to non existing location.+-}+++{-# INLINE listAll' #-}+listAll' :: Bool -> (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]+listAll' followSymlinks predicate path =+    handle (\(_ :: IOError) -> pure []) $ do+        file <- getFileStatus path+        if Posix.isDirectory file+            then do+                content <- listDir predicate path++                next <- unsafeInterleaveIO $ foldMapA (listAll' followSymlinks predicate) content+                pure $ content ++ next+            else pure []+  where+    {-# INLINE getFileStatus #-}+    getFileStatus+        | followSymlinks = Posix.getFileStatus+        | otherwise = Posix.getSymbolicLinkStatus+++{-# INLINE listAll'' #-}+listAll'' :: Bool -> (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]+listAll'' followSymlinks predicate path =+    (path :) <$> listAll' followSymlinks predicate path+++-- | Like 'list' but uses provided function to test in which 'RawFilePath' to recurse into.+listMatching :: (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]+listMatching =+    listAll'' False+++-- | Like 'followList' but uses provided function to test in which 'RawFilePath' to recurse into.+followListMatching :: (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]+followListMatching =+    listAll'' True+++{- | List all files, directories & symlinks recursively.+ Symlinks are not followed. See 'followList'.+-}+list :: RawFilePath -> IO [RawFilePath]+list =+    listAll'' False (const True)+++{- | List all files, directories & symlinks recursively.+ Unlike 'list', symlinks are followed recursively as well.+-}+followList :: RawFilePath -> IO [RawFilePath]+followList =+    listAll'' True (const True)+++{- $custom+ All /File Type Based Listing/ functions are based on top of this interface.+ This part of API exposes exposes access for writing custom filtering functions.++ All paths are tested for filter functions so unreadble files won't appear in the result list:++ > PosixRecursive.listCustom PosixRecursive.defConf "i-dont-exist"+ > >>> []++ It's not possible to turn of this behaviour because this functions must get the 'FileStatus'+ type which requires reading each entry.+-}+++-- | Configuration arguments for 'listCustom'.+data Conf = Conf+    { -- | Filter paths algorithm should recurse into+      filterPath :: !(RawFilePath -> Bool)+    , -- | Test if file should be included in returned List+      includeFile :: !(FileStatus -> RawFilePath -> IO Bool)+    , -- | Follow symbolic links+      followSymlinks :: !Bool+    }+++{- | Default 'Conf'iguration.++> listCustom defConf == listAccessible++* Recurses into all Paths+* Includes all file types+* Does __not__ follow symlinks+-}+defConf :: Conf+defConf =+    Conf+        { filterPath = const True+        , includeFile = \_ _ -> pure True+        , followSymlinks = False+        }+++{-# INLINE listAccessible' #-}+listAccessible' :: Conf -> RawFilePath -> IO [RawFilePath]+listAccessible' Conf{..} path =+    handle (\(_ :: IOError) -> pure []) $ do+        file <- getFileStatus path+        next <-+            if Posix.isDirectory file+                then do+                    content <- listDir filterPath path+                    unsafeInterleaveIO $ foldMapA (listAccessible' Conf{..}) content+                else pure []++        include <- includeFile file path+        if include+            then pure $ path : next+            else pure next+  where+    {-# INLINE getFileStatus #-}+    getFileStatus+        | followSymlinks = Posix.getFileStatus+        | otherwise = Posix.getSymbolicLinkStatus+++-- | Recursively list files using custom filters.+listCustom :: Conf -> RawFilePath -> IO [RawFilePath]+listCustom =+    listAccessible'+++{- | Like 'list' but automatically filters out inacessible files like broken symlins+or unreadable files and directories.+-}+listAccessible :: RawFilePath -> IO [RawFilePath]+listAccessible =+    listAccessible' defConf+++{- $type_based+ Functions for listing specific file type. Reading the file type requires+ ability to read the file.++  __These function do not return broken symlinks and inacessible files__++ Include test is applied even for the root entry (path past in as an argument).+ This means that non existing paths are filtered.++ > PosixRecursive.listDirectories "i-dont-exist"+ > >>> []+-}+++-- | List sub directories of given directory.+listDirectories :: RawFilePath -> IO [RawFilePath]+listDirectories =+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isDirectory file}+++-- | Lists only files (while recursing into directories still).+listRegularFiles :: RawFilePath -> IO [RawFilePath]+listRegularFiles =+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isRegularFile file}+++-- | Lists only symbolic links (while recursing into directories still).+listSymbolicLinks :: RawFilePath -> IO [RawFilePath]+listSymbolicLinks =+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isSymbolicLink file}
+ System/Posix/Recursive/ByteString/Unsafe.hs view
@@ -0,0 +1,242 @@+{- |+ Module      : System.Posix.Recursive.ByteString.Unsafe-- Copyright   : (c) Marek Fajkus+ License     : BSD3++ Maintainer  : marek.faj@gmail.com++ All modules profided by @unix-recursive@ expose similar API.+ Make sure you're using the module wich best fits your needs based on:+   - Working  with 'RawFilePath' (faster and more packed) or 'FilePath' (slower but easier to work with safely)+   - Exception free (Default) or @Unsafe@ variants of functions++ = Usage++ This module is designed to be imported as @qualified@:++ > import qualified System.Posix.Recursive.ByteString.Unsafe as PosixRecursive++ __Results__++ All functions return will return root path (the one they accept in argument) as a first item in the list:++ > head <$> PosixRecursive.list "System"+ > >>> "System"++ Other than that, this library __provides no guarantees about the order in which files appear in the resulting list__+ to make it possible to change the underlaying strategy in future versions.++ __Laziness__++ All IO operations are __guaranteed to be lazy and have constanct space characteristic__.+ Only the IO required by lazy computation will be performed so for instance running code like:++ > take 20 <$> PosixRecursive.listDirectories "/"++ Will perform only minimal ammount of IO needed to collect 20 directories on a root partition+-}+module System.Posix.Recursive.ByteString.Unsafe (+    -- * Basic Listing+    -- $basic_listing+    list,+    followList,+    listMatching,+    followListMatching,++    -- * File Type Based Listing+    -- $type_based+    listDirectories,+    listRegularFiles,+    listSymbolicLinks,++    -- * Custom Listing+    -- $custom+    Conf (..),+    defConf,+    listCustom,+) where++import Control.Exception (bracket)+import Data.Foldable (fold)+import System.IO.Unsafe (unsafeInterleaveIO)++import qualified Data.ByteString as BS+import System.Posix.ByteString.FilePath (RawFilePath)++import qualified System.Posix.Directory.ByteString as Posix+import qualified System.Posix.Files.ByteString as Posix++import System.Posix.Recursive.ByteString (Conf (..), defConf)+++-- Helpers++foldMapA :: (Monoid b, Traversable t, Applicative f) => (a -> f b) -> t a -> f b+foldMapA = (fmap fold .) . traverse+++{-# INLINE listDir #-}+listDir :: (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]+listDir predicate path =+    bracket+        (Posix.openDirStream path)+        Posix.closeDirStream+        (go [])+  where+    go :: [RawFilePath] -> Posix.DirStream -> IO [RawFilePath]+    go acc dirp = do+        e <- Posix.readDirStream dirp+        if BS.null e+            then pure acc+            else+                if e /= "." && e /= ".."+                    then+                        let fullPath = path <> "/" <> e+                         in if predicate fullPath+                                then go (fullPath : acc) dirp+                                else go acc dirp+                    else go acc dirp+{- $basic_listing+ Functions for listing contents of directory recursively.+ These functions list all the content they encounter while traversing+ the file system tree including directories, files, symlinks, broken symlinks.++  __Functions from this module will throw 'IOError' if it can't open the directory__+ (i.e. becacuse permission error or other process removing the given path).++ Functions from this section is gurantee to always return the root path as a first element even+ if this path does not exist.++ > PosixRecursive.list "i-dont-exist"+ > >>> ["i-dont-exist"]++ In these cases the root path is considered the same way as symlink+ to non existing location.+-}+++{-# INLINE listAll' #-}+listAll' :: Bool -> (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]+listAll' followSymlinks predicate path =+    do+        file <- getFileStatus path+        if Posix.isDirectory file+            then do+                content <- listDir predicate path++                next <- unsafeInterleaveIO $ foldMapA (listAll' followSymlinks predicate) content+                pure $ content ++ next+            else pure []+  where+    {-# INLINE getFileStatus #-}+    getFileStatus+        | followSymlinks = Posix.getFileStatus+        | otherwise = Posix.getSymbolicLinkStatus+++{-# INLINE listAll'' #-}+listAll'' :: Bool -> (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]+listAll'' followSymlinks predicate path =+    (path :) <$> listAll' followSymlinks predicate path+++-- | Like 'list' but uses provided function to test in which 'RawFilePath' to recurse into.+listMatching :: (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]+listMatching =+    listAll'' False+++-- | Like 'followList' but uses provided function to test in which 'RawFilePath' to recurse into.+followListMatching :: (RawFilePath -> Bool) -> RawFilePath -> IO [RawFilePath]+followListMatching =+    listAll'' True+++{- | List all files, directories & symlinks recursively.+ Symlinks are not followed. See 'followList'.+-}+list :: RawFilePath -> IO [RawFilePath]+list =+    listAll'' False (const True)+++{- | List all files, directories & symlinks recursively.+ Unlike 'list', symlinks are followed recursively as well.+-}+followList :: RawFilePath -> IO [RawFilePath]+followList =+    listAll'' True (const True)+++{- $custom+ All /File Type Based Listing/ functions are based on top of this interface.+ This part of API exposes exposes access for writing custom filtering functions.++ All paths are tested for filter functions so unreadble files won't appear in the result list:++ > PosixRecursive.listCustom PosixRecursive.defConf "i-dont-exist"+ > >>> []++ It's not possible to turn of this behaviour because this functions must get the 'FileStatus'+ type which requires reading each entry.+-}+++{-# INLINE listAccessible' #-}+listAccessible' :: Conf -> RawFilePath -> IO [RawFilePath]+listAccessible' Conf{..} path =+    do+        file <- getFileStatus path+        next <-+            if Posix.isDirectory file+                then do+                    content <- listDir filterPath path+                    unsafeInterleaveIO $ foldMapA (listAccessible' Conf{..}) content+                else pure []++        include <- includeFile file path+        if include+            then pure $ path : next+            else pure next+  where+    {-# INLINE getFileStatus #-}+    getFileStatus+        | followSymlinks = Posix.getFileStatus+        | otherwise = Posix.getSymbolicLinkStatus+++-- | Recursively list files using custom filters.+listCustom :: Conf -> RawFilePath -> IO [RawFilePath]+listCustom =+    listAccessible'+++{- $type_based+ Functions for listing specific file type. Reading the file type requires+ ability to read the file.++  __These functions will throw 'IOError' when tring to open unreadable file.__++ Include test is applied even for the root entry (path past in as an argument).+ This means that non existing paths are filtered.++ > PosixRecursive.listDirectories "i-dont-exist"+ > >>> []+-}+++-- | List sub directories of given directory.+listDirectories :: RawFilePath -> IO [RawFilePath]+listDirectories =+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isDirectory file}+++-- | Lists only files (while recursing into directories still).+listRegularFiles :: RawFilePath -> IO [RawFilePath]+listRegularFiles =+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isRegularFile file}+++-- | Lists only symbolic links (while recursing into directories still).+listSymbolicLinks :: RawFilePath -> IO [RawFilePath]+listSymbolicLinks =+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isSymbolicLink file}
+ System/Posix/Recursive/Unsafe.hs view
@@ -0,0 +1,238 @@+{- |+ Module      : System.Posix.Recursive.Unsafe-- Copyright   : (c) Marek Fajkus+ License     : BSD3++ Maintainer  : marek.faj@gmail.com++ All modules profided by @unix-recursive@ expose similar API.+ Make sure you're using the module wich best fits your needs based on:+   - Working  with 'RawFilePath' (faster and more packed) or 'FilePath' (slower but easier to work with safely)+   - Exception free (Default) or @Unsafe@ variants of functions++ = Usage++ This module is designed to be imported as @qualified@:++ > import qualified System.Posix.Recursive.Unsafe as PosixRecursive++ __Results__++ All functions return will return root path (the one they accept in argument) as a first item in the list:++ > head <$> PosixRecursive.list "System"+ > >>> "System"++ Other than that, this library __provides no guarantees about the order in which files appear in the resulting list__+ to make it possible to change the underlaying strategy in future versions.++ __Laziness__++ All IO operations are __guaranteed to be lazy and have constanct space characteristic__.+ Only the IO required by lazy computation will be performed so for instance running code like:++ > take 20 <$> PosixRecursive.listDirectories "/"++ Will perform only minimal ammount of IO needed to collect 20 directories on a root partition+-}+module System.Posix.Recursive.Unsafe (+    -- * Basic Listing+    -- $basic_listing+    list,+    followList,+    listMatching,+    followListMatching,++    -- * File Type Based Listing+    -- $type_based+    listDirectories,+    listRegularFiles,+    listSymbolicLinks,++    -- * Custom Listing+    -- $custom+    Conf (..),+    defConf,+    listCustom,+) where++import Control.Exception (bracket)+import Data.Foldable (fold)+import System.IO.Unsafe (unsafeInterleaveIO)++import qualified System.Posix.Directory as Posix+import qualified System.Posix.Files as Posix+import System.Posix.Recursive (Conf (..), defConf)+++-- Helpers++foldMapA :: (Monoid b, Traversable t, Applicative f) => (a -> f b) -> t a -> f b+foldMapA = (fmap fold .) . traverse+++{-# INLINE listDir #-}+listDir :: (FilePath -> Bool) -> FilePath -> IO [FilePath]+listDir predicate path =+    bracket+        (Posix.openDirStream path)+        Posix.closeDirStream+        (go [])+  where+    go :: [FilePath] -> Posix.DirStream -> IO [FilePath]+    go acc dirp = do+        e <- Posix.readDirStream dirp+        if null e+            then pure acc+            else+                if e /= "." && e /= ".."+                    then+                        let fullPath = path <> "/" <> e+                         in if predicate fullPath+                                then go (fullPath : acc) dirp+                                else go acc dirp+                    else go acc dirp+{- $basic_listing+ Functions for listing contents of directory recursively.+ These functions list all the content they encounter while traversing+ the file system tree including directories, files, symlinks, broken symlinks.++  __Functions from this module will throw 'IOError' if it can't open the directory__+ (i.e. becacuse permission error or other process removing the given path).++ Functions from this section is gurantee to always return the root path as a first element even+ if this path does not exist.++ > PosixRecursive.list "i-dont-exist"+ > >>> ["i-dont-exist"]++ In these cases the root path is considered the same way as symlink+ to non existing location.+-}+++{-# INLINE listAll' #-}+listAll' :: Bool -> (FilePath -> Bool) -> FilePath -> IO [FilePath]+listAll' followSymlinks predicate path =+    do+        file <- getFileStatus path+        if Posix.isDirectory file+            then do+                content <- listDir predicate path++                next <- unsafeInterleaveIO $ foldMapA (listAll' followSymlinks predicate) content+                pure $ content ++ next+            else pure []+  where+    {-# INLINE getFileStatus #-}+    getFileStatus+        | followSymlinks = Posix.getFileStatus+        | otherwise = Posix.getSymbolicLinkStatus+++{-# INLINE listAll'' #-}+listAll'' :: Bool -> (FilePath -> Bool) -> FilePath -> IO [FilePath]+listAll'' followSymlinks predicate path =+    (path :) <$> listAll' followSymlinks predicate path+++-- | Like 'list' but uses provided function to test in which 'FilePath' to recurse into.+listMatching :: (FilePath -> Bool) -> FilePath -> IO [FilePath]+listMatching =+    listAll'' False+++-- | Like 'followList' but uses provided function to test in which 'FilePath' to recurse into.+followListMatching :: (FilePath -> Bool) -> FilePath -> IO [FilePath]+followListMatching =+    listAll'' True+++{- | List all files, directories & symlinks recursively.+ Symlinks are not followed. See 'followList'.+-}+list :: FilePath -> IO [FilePath]+list =+    listAll'' False (const True)+++{- | List all files, directories & symlinks recursively.+ Unlike 'list', symlinks are followed recursively as well.+-}+followList :: FilePath -> IO [FilePath]+followList =+    listAll'' True (const True)+++{- $custom+ All /File Type Based Listing/ functions are based on top of this interface.+ This part of API exposes exposes access for writing custom filtering functions.++ All paths are tested for filter functions so unreadble files won't appear in the result list:++ > PosixRecursive.listCustom PosixRecursive.defConf "i-dont-exist"+ > >>> []++ It's not possible to turn of this behaviour because this functions must get the 'FileStatus'+ type which requires reading each entry.+-}+++{-# INLINE listAccessible' #-}+listAccessible' :: Conf -> FilePath -> IO [FilePath]+listAccessible' Conf{..} path =+    do+        file <- getFileStatus path+        next <-+            if Posix.isDirectory file+                then do+                    content <- listDir filterPath path+                    unsafeInterleaveIO $ foldMapA (listAccessible' Conf{..}) content+                else pure []++        include <- includeFile file path+        if include+            then pure $ path : next+            else pure next+  where+    {-# INLINE getFileStatus #-}+    getFileStatus+        | followSymlinks = Posix.getFileStatus+        | otherwise = Posix.getSymbolicLinkStatus+++-- | Recursively list files using custom filters.+listCustom :: Conf -> FilePath -> IO [FilePath]+listCustom =+    listAccessible'+++{- $type_based+ Functions for listing specific file type. Reading the file type requires+ ability to read the file.++  __These functions will throw 'IOError' when tring to open unreadable file.__++ Include test is applied even for the root entry (path past in as an argument).+ This means that non existing paths are filtered.++ > PosixRecursive.listDirectories "i-dont-exist"+ > >>> []+-}+++-- | List sub directories of given directory.+listDirectories :: FilePath -> IO [FilePath]+listDirectories =+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isDirectory file}+++-- | Lists only files (while recursing into directories still).+listRegularFiles :: FilePath -> IO [FilePath]+listRegularFiles =+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isRegularFile file}+++-- | Lists only symbolic links (while recursing into directories still).+listSymbolicLinks :: FilePath -> IO [FilePath]+listSymbolicLinks =+    listAccessible' defConf{includeFile = \file _ -> pure $ Posix.isSymbolicLink file}
+ bench/Bench.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import Criterion.Main++import System.Directory.Recursive as DirTraverse+import qualified System.Posix.Recursive as String+import qualified System.Posix.Recursive.ByteString as ByteString+++main :: IO ()+main =+    defaultMain+        [ bgroup+            "compare different api groups"+            [ bgroup+                "ByteString"+                [ bench "filtering base on path" $ nfIO $ ByteString.list "."+                , bench "filter by path and file status" $ nfIO $ ByteString.listAccessible "."+                ]+            , bgroup+                "String"+                [ bench "filtering base on path" $ nfIO $ String.list "."+                , bench "filter by path and file status" $ nfIO $ String.listAccessible "."+                ]+            ]+        , bgroup+            "compare with dir traverse"+            [ bgroup+                "Evaluate whole list"+                [ bench "unix-recursive String" $ nfIO $ String.followList "."+                , bench "unix-recursive ByteString" $ nfIO $ ByteString.followList "."+                , bench "dir-traverse" $ nfIO $ DirTraverse.getDirRecursive "."+                ]+            , bgroup+                "Test laziness"+                [ bench "unix-recursive String" $ nfIO (head . tail <$> String.followList ".")+                , bench "unix-recursive ByteString" $ nfIO (head . tail <$> ByteString.followList ".")+                , bench "dir-traverse" $ nfIO ((head . tail) <$> DirTraverse.getDirRecursive ".")+                ]+            ]+        ]
+ bin/dir-traverse.hs view
@@ -0,0 +1,11 @@+module Main where++import System.Directory.Recursive as Lib+import System.Environment (getArgs)+++main :: IO ()+main = do+    [path] <- getArgs+    all <- Lib.getDirRecursive path+    print $ length all
+ bin/dirstream.hs view
@@ -0,0 +1,32 @@+module Main where++import Control.Applicative+import Data.DirStream+import Data.String+import qualified Filesystem.Path as F+import Pipes+import Pipes.Prelude hiding (print)+import Pipes.Safe+import System.Environment+import Prelude hiding (length)+++dirstreamGet :: F.FilePath -> ListT (SafeT IO) F.FilePath+dirstreamGet path = do+    child <- childOf path+    isDir <- liftIO $ isDirectory child+    if isDir+        then pure child <|> descendentOf child+        else pure child+++main :: IO ()+main = do+    [path] <- getArgs++    count <-+        runSafeT $+            runEffect $+                (length (every (descendentOf (fromString path))))++    print count
+ bin/unix-recursive-bytestring.hs view
@@ -0,0 +1,13 @@+module Main where++import qualified Data.ByteString.UTF8 as BS+import System.Environment (getArgs)+import qualified System.Posix.Recursive.ByteString as Lib+++main :: IO ()+main = do+    [path'] <- getArgs+    let path = BS.fromString path'+    all <- Lib.followList path+    print $ length all
+ bin/unix-recursive-string.hs view
@@ -0,0 +1,11 @@+module Main where++import System.Environment (getArgs)+import qualified System.Posix.Recursive as Lib+++main :: IO ()+main = do+    [path] <- getArgs+    all <- Lib.followList path+    print $ length all
+ test/Spec.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}++import Data.List (sort)++import Spec.Listing+import Test.Hspec --(anyException, context, hspec, it, shouldThrow)++import System.IO.Error (isPermissionError)+import System.Posix.ByteString.FilePath (RawFilePath)++import qualified Data.ByteString as BS+import qualified System.Posix.Recursive as String+import qualified System.Posix.Recursive.ByteString as ByteString+import qualified System.Posix.Recursive.ByteString.Unsafe as UnsafeBS+import qualified System.Posix.Recursive.Unsafe as Unsafe+++-- Helper+isSuffixOf :: String -> String -> Bool+isSuffixOf needle haystack+    | needleLen > hayLen = False+    | otherwise = needle == drop (hayLen - needleLen) haystack+  where+    needleLen = length needle+    hayLen = length haystack+++instance DirectoryListing FilePath where+    list = String.list+    followList = String.followList+    listMatching = String.listMatching+    followListMatching = String.followListMatching+    listAccessible = String.listAccessible+    listDirectories = String.listDirectories+    listRegularFiles = String.listRegularFiles+    listSymbolicLinks = String.listSymbolicLinks+    listCustom filterPath includeFile =+        String.listCustom+            String.defConf+                { String.filterPath = filterPath+                , String.includeFile = includeFile+                }+++instance DirectoryListing RawFilePath where+    list = ByteString.list+    followList = ByteString.followList+    listMatching = ByteString.listMatching+    followListMatching = ByteString.followListMatching+    listDirectories = ByteString.listDirectories+    listAccessible = ByteString.listAccessible+    listRegularFiles = ByteString.listRegularFiles+    listSymbolicLinks = ByteString.listSymbolicLinks+    listCustom filterPath includeFile =+        ByteString.listCustom+            ByteString.defConf+                { ByteString.filterPath = filterPath+                , ByteString.includeFile = includeFile+                }+++main :: IO ()+main =+    hspec $ do+        context "System.Posix.Recursive" $ spec isSuffixOf++        context "System.Posix.Recursive.Unsafe" $+            it "should throw" $ do+                let exp = do+                        res <- Unsafe.list "test/workdir"+                        print $ length res+                exp `shouldThrow` isPermissionError++        context "System.Posix.Recursive.ByteString" $ spec BS.isSuffixOf++        context "System.Posix.Recursive.ByteString.Unsafe" $+            it "should throw" $ do+                let exp = do+                        res <- UnsafeBS.list "test/workdir"+                        print $ length res+                exp `shouldThrow` isPermissionError
+ test/Spec/Listing.hs view
@@ -0,0 +1,242 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Spec.Listing (spec, DirectoryListing (..)) where++import Data.List (sort)+import Data.String+import System.Posix.Files (FileStatus)+import qualified System.Posix.Files as Posix+import Test.Hspec+++class (Show a, Ord a, IsString a) => DirectoryListing a where+    list :: a -> IO [a]+    followList :: a -> IO [a]+    listMatching :: (a -> Bool) -> a -> IO [a]+    followListMatching :: (a -> Bool) -> a -> IO [a]+    listAccessible :: a -> IO [a]+    listDirectories :: a -> IO [a]+    listRegularFiles :: a -> IO [a]+    listSymbolicLinks :: a -> IO [a]+    listCustom :: (a -> Bool) -> (FileStatus -> a -> IO Bool) -> a -> IO [a]+++spec :: forall a. DirectoryListing a => (a -> a -> Bool) -> Spec+spec filter = do+    context "Not following symlinks" $ do+        describe "listEverything" $ do+            it "returns expected list of everything in workdir" $ do+                res :: [a] <- list "test/workdir"+                let expected =+                        [ "test/workdir"+                        , "test/workdir/dir1"+                        , "test/workdir/dir1/roots-dir1"+                        , "test/workdir/dir1/roots-dir1/roots-file1"+                        , "test/workdir/dir1/sub1"+                        , "test/workdir/dir1/sub1/file1"+                        , "test/workdir/dir1/sub1/file2"+                        , "test/workdir/dir1/sub2"+                        , "test/workdir/dir1/sub2/file1"+                        , "test/workdir/dir1/sub2/file2"+                        , "test/workdir/dir1/sub2/file3"+                        , "test/workdir/dir1/sub2/roots-file1"+                        , "test/workdir/dir2"+                        , "test/workdir/dir2/file1"+                        , "test/workdir/dir2/sym1"+                        , "test/workdir/dir2/sym2"+                        , "test/workdir/dir3"+                        , "test/workdir/dir3/sub1"+                        , "test/workdir/dir3/sub1/file1"+                        , "test/workdir/dir3/sub2"+                        , "test/workdir/dir3/sub2/broken-sym1"+                        , "test/workdir/file1"+                        , "test/workdir/file2"+                        , "test/workdir/only-roots-dir1"+                        ]+                res `shouldMatchList` expected++        describe "listAll" $ do+            it "returns list excluding files & dirs ending with `3`" $ do+                res :: [a] <- listMatching (not . filter "3") "test/workdir"+                let expected =+                        [ "test/workdir"+                        , "test/workdir/dir1"+                        , "test/workdir/dir1/roots-dir1"+                        , "test/workdir/dir1/roots-dir1/roots-file1"+                        , "test/workdir/dir1/sub1"+                        , "test/workdir/dir1/sub1/file1"+                        , "test/workdir/dir1/sub1/file2"+                        , "test/workdir/dir1/sub2"+                        , "test/workdir/dir1/sub2/file1"+                        , "test/workdir/dir1/sub2/file2"+                        , "test/workdir/dir1/sub2/roots-file1"+                        , "test/workdir/dir2"+                        , "test/workdir/dir2/file1"+                        , "test/workdir/dir2/sym1"+                        , "test/workdir/dir2/sym2"+                        , "test/workdir/file1"+                        , "test/workdir/file2"+                        , "test/workdir/only-roots-dir1"+                        ]+                res `shouldMatchList` expected++        describe "listEverythingAccessible" $ do+            it "returns everything that is accessible in workdir" $ do+                res :: [a] <- listAccessible "test/workdir"+                let expected =+                        [ "test/workdir"+                        , "test/workdir/dir1"+                        , "test/workdir/dir1/roots-dir1"+                        , "test/workdir/dir1/roots-dir1/roots-file1"+                        , "test/workdir/dir1/sub1"+                        , "test/workdir/dir1/sub1/file1"+                        , "test/workdir/dir1/sub1/file2"+                        , "test/workdir/dir1/sub2"+                        , "test/workdir/dir1/sub2/file1"+                        , "test/workdir/dir1/sub2/file2"+                        , "test/workdir/dir1/sub2/file3"+                        , "test/workdir/dir1/sub2/roots-file1"+                        , "test/workdir/dir2"+                        , "test/workdir/dir2/file1"+                        , "test/workdir/dir2/sym1"+                        , "test/workdir/dir2/sym2"+                        , "test/workdir/dir3"+                        , "test/workdir/dir3/sub1"+                        , "test/workdir/dir3/sub1/file1"+                        , "test/workdir/dir3/sub2"+                        , "test/workdir/dir3/sub2/broken-sym1"+                        , "test/workdir/file1"+                        , "test/workdir/file2"+                        ]+                res `shouldMatchList` expected++        describe "listDirectories" $ do+            it "returns only directories" $ do+                res :: [a] <- listDirectories "test/workdir"+                let expected =+                        [ "test/workdir"+                        , "test/workdir/dir1"+                        , "test/workdir/dir1/roots-dir1"+                        , "test/workdir/dir1/sub1"+                        , "test/workdir/dir1/sub2"+                        , "test/workdir/dir2"+                        , "test/workdir/dir3"+                        , "test/workdir/dir3/sub1"+                        , "test/workdir/dir3/sub2"+                        ]+                res `shouldMatchList` expected++        describe "listFiles" $ do+            it "returns only files" $ do+                res :: [a] <- listRegularFiles "test/workdir"+                let expected =+                        [ "test/workdir/dir1/roots-dir1/roots-file1"+                        , "test/workdir/dir1/sub1/file1"+                        , "test/workdir/dir1/sub1/file2"+                        , "test/workdir/dir1/sub2/file1"+                        , "test/workdir/dir1/sub2/file2"+                        , "test/workdir/dir1/sub2/file3"+                        , "test/workdir/dir1/sub2/roots-file1"+                        , "test/workdir/dir2/file1"+                        , "test/workdir/dir3/sub1/file1"+                        , "test/workdir/file1"+                        , "test/workdir/file2"+                        ]+                res `shouldMatchList` expected++        describe "listSymbolicLinks" $ do+            it "returns only symlinks" $ do+                res :: [a] <- listSymbolicLinks "test/workdir"+                let expected =+                        [ "test/workdir/dir2/sym1"+                        , "test/workdir/dir2/sym2"+                        , "test/workdir/dir3/sub2/broken-sym1"+                        ]+                res `shouldMatchList` expected++        describe "listCustom" $ do+            it "return everything except for symlinks and not within dir1" $ do+                res :: [a] <-+                    listCustom+                        (not . filter "dir1")+                        (\file _ -> pure $ not $ Posix.isSymbolicLink file)+                        "test/workdir"+                let expected =+                        [ "test/workdir"+                        , "test/workdir/dir2"+                        , "test/workdir/dir2/file1"+                        , "test/workdir/dir3"+                        , "test/workdir/dir3/sub1"+                        , "test/workdir/dir3/sub1/file1"+                        , "test/workdir/dir3/sub2"+                        , "test/workdir/file1"+                        , "test/workdir/file2"+                        ]+                res `shouldMatchList` expected++    context "Following symlinks" $ do+        describe "followListEverything" $ do+            it "returns expected list of everything in workdir" $ do+                res :: [a] <- followList "test/workdir"+                let expected =+                        [ "test/workdir"+                        , "test/workdir/dir1"+                        , "test/workdir/dir1/roots-dir1"+                        , "test/workdir/dir1/roots-dir1/roots-file1"+                        , "test/workdir/dir1/sub1"+                        , "test/workdir/dir1/sub1/file1"+                        , "test/workdir/dir1/sub1/file2"+                        , "test/workdir/dir1/sub2"+                        , "test/workdir/dir1/sub2/file1"+                        , "test/workdir/dir1/sub2/file2"+                        , "test/workdir/dir1/sub2/file3"+                        , "test/workdir/dir1/sub2/roots-file1"+                        , "test/workdir/dir2"+                        , "test/workdir/dir2/file1"+                        , "test/workdir/dir2/sym1"+                        , "test/workdir/dir2/sym2"+                        , "test/workdir/dir2/sym2/sub1"+                        , "test/workdir/dir2/sym2/sub1/file1"+                        , "test/workdir/dir2/sym2/sub2"+                        , "test/workdir/dir2/sym2/sub2/broken-sym1"+                        , "test/workdir/dir3"+                        , "test/workdir/dir3/sub1"+                        , "test/workdir/dir3/sub1/file1"+                        , "test/workdir/dir3/sub2"+                        , "test/workdir/dir3/sub2/broken-sym1"+                        , "test/workdir/file1"+                        , "test/workdir/file2"+                        , "test/workdir/only-roots-dir1"+                        ]+                res `shouldMatchList` expected++        describe "followListAll" $ do+            it "returns list excluding files & dirs ending with `3`" $ do+                res <- followListMatching (not . filter "3") "test/workdir"+                let expected =+                        [ "test/workdir"+                        , "test/workdir/dir1"+                        , "test/workdir/dir1/roots-dir1"+                        , "test/workdir/dir1/roots-dir1/roots-file1"+                        , "test/workdir/dir1/sub1"+                        , "test/workdir/dir1/sub1/file1"+                        , "test/workdir/dir1/sub1/file2"+                        , "test/workdir/dir1/sub2"+                        , "test/workdir/dir1/sub2/file1"+                        , "test/workdir/dir1/sub2/file2"+                        , "test/workdir/dir1/sub2/roots-file1"+                        , "test/workdir/dir2"+                        , "test/workdir/dir2/file1"+                        , "test/workdir/dir2/sym1"+                        , "test/workdir/dir2/sym2"+                        , "test/workdir/dir2/sym2/sub1"+                        , "test/workdir/dir2/sym2/sub1/file1"+                        , "test/workdir/dir2/sym2/sub2"+                        , "test/workdir/dir2/sym2/sub2/broken-sym1"+                        , "test/workdir/file1"+                        , "test/workdir/file2"+                        , "test/workdir/only-roots-dir1"+                        ]+                res `shouldMatchList` expected
+ unix-recursive.cabal view
@@ -0,0 +1,118 @@+cabal-version:       >=1.12++name:                unix-recursive+version:             0.1.0.0+synopsis:            Fast and flexible primitives for recursive file system IO on Posix systems+description:+    Blazingly fast functions for recursive file system operations.+    Utilizing lazy IO for constant space & computation efficiant bindigns to Posix dirstream.h api.++bug-reports:         https://github.com/turboMaCk/unix-recursive/issues+license:             BSD3+license-file:        LICENSE+author:              Marek Fajkus <marek.faj@gmail.com>+maintainer:          Marek Fajkus <marek.faj@gmail.com>+copyright:           2021 Marek Fajkus+stability:           experimental+category:            System, Filesystem+build-type:          Simple+extra-source-files:  README.md+                   , ChangeLog.md+homepage:            https://github.com/turboMaCk/unix-recursive++source-repository head+  type: git+  location: https://github.com/turboMaCk/unix-recursive++Flag bin+  default: False+  manual: True++library+    exposed-modules:      System.Posix.Recursive+                        , System.Posix.Recursive.Unsafe+                        , System.Posix.Recursive.ByteString+                        , System.Posix.Recursive.ByteString.Unsafe+    other-modules:        Paths_unix_recursive+    hs-source-dirs:       .+    build-depends:        base >=4.7 && <5+                        , unix+                        , bytestring+    default-language:     Haskell2010+    default-extensions:   OverloadedStrings+                        , RecordWildCards+                        , ScopedTypeVariables++executable dir-traverse-bin+    main-is:            dir-traverse.hs+    hs-source-dirs:     bin+    ghc-options:        -threaded -rtsopts -with-rtsopts=-N -Wall+    build-depends:      base >=4.7 && <5+                      , dir-traverse == 0.2.3.0+    default-language:   Haskell2010+    if !flag(bin)+        buildable: False++executable dirstream-bin+    main-is:            dirstream.hs+    hs-source-dirs:     bin+    ghc-options:        -threaded -rtsopts -with-rtsopts=-N -Wall+    build-depends:      base >=4.7 && <5+                      , dirstream+                      , pipes+                      , pipes-safe+                      , system-filepath+                      , directory+    default-language:   Haskell2010+    if !flag(bin)+        buildable: False++executable unix-recursive-string-bin+    main-is:            unix-recursive-string.hs+    other-modules:      Paths_unix_recursive+    hs-source-dirs:     bin+    ghc-options:        -threaded -rtsopts -with-rtsopts=-N -Wall+    build-depends:      base >=4.7 && <5+                      , unix-recursive+                      , unix+    default-language:   Haskell2010+    if !flag(bin)+        buildable: False++executable unix-recursive-bytestring-bin+    main-is:            unix-recursive-bytestring.hs+    other-modules:      Paths_unix_recursive+    hs-source-dirs:     bin+    ghc-options:        -threaded -rtsopts -with-rtsopts=-N -Wall+    build-depends:      base >=4.7 && <5+                      , unix-recursive+                      , unix+                      , utf8-string+    default-language:   Haskell2010+    if !flag(bin)+        buildable: False++benchmark unix-recursive-bench+    type:               exitcode-stdio-1.0+    main-is:            Bench.hs+    hs-source-dirs:     bench+    default-language:   Haskell2010+    ghc-options:        -Wall+    build-depends:      base >=4.7 && <5+                      , unix-recursive+                      , criterion+                      , dir-traverse == 0.2.3.0++test-suite unix-recursive-test+    type:               exitcode-stdio-1.0+    main-is:            Spec.hs+    other-modules:      Paths_unix_recursive+                      , Spec.Listing+    hs-source-dirs:     test+    ghc-options:        -threaded -rtsopts -with-rtsopts=-N -Wall+    build-depends:      base >=4.7 && <5+                      , unix-recursive+                      , hspec+                      , bytestring+                      , unix+    default-language:   Haskell2010