    {-# LANGUAGE OverloadedStrings #-}

    -- | File finding utiliites for Shelly
    -- The basic 'find' takes a dir and gives back a list of files.
    -- If you don't just want a list, use the folding variants like 'findFold'.
    -- If you want to avoid traversing certain directories, use the directory filtering variants like 'findDirFilter'

    module Shelly.Find
      ( find
      , findWhen
      , findFold
      , findDirFilter
      , findDirFilterWhen
      , findFoldDirFilter
      )
    where

    import           Shelly.Base
    import           Control.Monad                  ( foldM )
    #if !MIN_VERSION_base(4,13,0)
    import           Data.Monoid                    ( mappend )
    #endif
    import           System.PosixCompat.Files       ( getSymbolicLinkStatus
                                                    , isSymbolicLink
                                                    )
    import           System.Directory               ( doesDirectoryExist )

    -- | List directory recursively (like the POSIX utility "find").
    -- listing is relative if the path given is relative.
    -- If you want to filter out some results or fold over them you can do that with the returned files.
    -- A more efficient approach is to use one of the other find functions.
    find :: FilePath -> Sh [FilePath]
    find :: FilePath -> Sh [FilePath]
    find = ([FilePath] -> FilePath -> Sh [FilePath])
    -> [FilePath] -> FilePath -> Sh [FilePath]
    forall a. (a -> FilePath -> Sh a) -> a -> FilePath -> Sh a
    findFold (\[FilePath]
    paths FilePath
    fp -> [FilePath] -> Sh [FilePath]
    forall a. a -> Sh a
    forall (m :: * -> *) a. Monad m => a -> m a
    return ([FilePath] -> Sh [FilePath]) -> [FilePath] -> Sh [FilePath]
    forall a b. (a -> b) -> a -> b
    $ [FilePath]
    paths [FilePath] -> [FilePath] -> [FilePath]
    forall a. [a] -> [a] -> [a]
    ++ [FilePath
    fp]) []

    -- | 'find' that filters the found files as it finds.
    -- Files must satisfy the given filter to be returned in the result.
    findWhen :: (FilePath -> Sh Bool) -> FilePath -> Sh [FilePath]
    findWhen :: (FilePath -> Sh Bool) -> FilePath -> Sh [FilePath]
    findWhen = (FilePath -> Sh Bool)
    -> (FilePath -> Sh Bool) -> FilePath -> Sh [FilePath]
    findDirFilterWhen (Sh Bool -> FilePath -> Sh Bool
    forall a b. a -> b -> a
    const (Sh Bool -> FilePath -> Sh Bool) -> Sh Bool -> FilePath -> Sh Bool
    forall a b. (a -> b) -> a -> b
    $ Bool -> Sh Bool
    forall a. a -> Sh a
    forall (m :: * -> *) a. Monad m => a -> m a
    return Bool
    True)

    -- | Fold an arbitrary folding function over files froma a 'find'.
    -- Like 'findWhen' but use a more general fold rather than a filter.
    findFold :: (a -> FilePath -> Sh a) -> a -> FilePath -> Sh a
    findFold :: forall a. (a -> FilePath -> Sh a) -> a -> FilePath -> Sh a
    findFold a -> FilePath -> Sh a
    folder a
    startValue =
      (a -> FilePath -> Sh a)
    -> a -> (FilePath -> Sh Bool) -> FilePath -> Sh a
    forall a.
    (a -> FilePath -> Sh a)
    -> a -> (FilePath -> Sh Bool) -> FilePath -> Sh a
    findFoldDirFilter a -> FilePath -> Sh a
    folder a
    startValue (Sh Bool -> FilePath -> Sh Bool
    forall a b. a -> b -> a
    const (Sh Bool -> FilePath -> Sh Bool) -> Sh Bool -> FilePath -> Sh Bool
    forall a b. (a -> b) -> a -> b
    $ Bool -> Sh Bool
    forall a. a -> Sh a
    forall (m :: * -> *) a. Monad m => a -> m a
    return Bool
    True)

    -- | 'find' that filters out directories as it finds.
    -- Filtering out directories can make a find much more efficient by avoiding entire trees of files.
    findDirFilter :: (FilePath -> Sh Bool) -> FilePath -> Sh [FilePath]
    findDirFilter :: (FilePath -> Sh Bool) -> FilePath -> Sh [FilePath]
    findDirFilter FilePath -> Sh Bool
    filt = (FilePath -> Sh Bool)
    -> (FilePath -> Sh Bool) -> FilePath -> Sh [FilePath]
    findDirFilterWhen FilePath -> Sh Bool
    filt (Sh Bool -> FilePath -> Sh Bool
    forall a b. a -> b -> a
    const (Sh Bool -> FilePath -> Sh Bool) -> Sh Bool -> FilePath -> Sh Bool
    forall a b. (a -> b) -> a -> b
    $ Bool -> Sh Bool
    forall a. a -> Sh a
    forall (m :: * -> *) a. Monad m => a -> m a
    return Bool
    True)

    -- | Similar to 'findWhen', but also filter out directories.
    -- Alternatively, similar to 'findDirFilter', but also filter out files.
    -- Filtering out directories makes the find much more efficient.
    findDirFilterWhen
      :: (FilePath -> Sh Bool) -- ^ directory filter
      -> (FilePath -> Sh Bool) -- ^ file filter
      -> FilePath -- ^ directory
      -> Sh [FilePath]
    findDirFilterWhen :: (FilePath -> Sh Bool)
    -> (FilePath -> Sh Bool) -> FilePath -> Sh [FilePath]
    findDirFilterWhen FilePath -> Sh Bool
    dirFilt FilePath -> Sh Bool
    fileFilter = ([FilePath] -> FilePath -> Sh [FilePath])
    -> [FilePath] -> (FilePath -> Sh Bool) -> FilePath -> Sh [FilePath]
    forall a.
    (a -> FilePath -> Sh a)
    -> a -> (FilePath -> Sh Bool) -> FilePath -> Sh a
    findFoldDirFilter [FilePath] -> FilePath -> Sh [FilePath]
    filterIt [] FilePath -> Sh Bool
    dirFilt
     where
      filterIt :: [FilePath] -> FilePath -> Sh [FilePath]
    filterIt [FilePath]
    paths FilePath
    fp = do
        yes <- FilePath -> Sh Bool
    fileFilter FilePath
    fp
        return $ if yes then paths ++ [fp] else paths

    -- | Like 'findDirFilterWhen' but use a folding function rather than a filter.
    -- The most general finder: you likely want a more specific one.
    findFoldDirFilter
      :: (a -> FilePath -> Sh a) -> a -> (FilePath -> Sh Bool) -> FilePath -> Sh a
    findFoldDirFilter :: forall a.
    (a -> FilePath -> Sh a)
    -> a -> (FilePath -> Sh Bool) -> FilePath -> Sh a
    findFoldDirFilter a -> FilePath -> Sh a
    folder a
    startValue FilePath -> Sh Bool
    dirFilter FilePath
    dir = do
      absDir <- FilePath -> Sh FilePath
    absPath FilePath
    dir
      trace ("find " `mappend` toTextIgnore absDir)
      filt <- dirFilter absDir
      if not filt
        then return startValue
        -- use possible relative path, not absolute so that listing will remain relative
        else do
          (rPaths, aPaths) <- lsRelAbs dir
          foldM traverse' startValue (zip rPaths aPaths)
     where
      traverse' :: a -> (FilePath, FilePath) -> Sh a
    traverse' a
    acc (FilePath
    relativePath, FilePath
    absolutePath) = do
        -- optimization: don't use Shelly API since our path is already good
        isDir  <- IO Bool -> Sh Bool
    forall a. IO a -> Sh a
    forall (m :: * -> *) a. MonadIO m => IO a -> m a
    liftIO (IO Bool -> Sh Bool) -> IO Bool -> Sh Bool
    forall a b. (a -> b) -> a -> b
    $ FilePath -> IO Bool
    doesDirectoryExist FilePath
    absolutePath
        sym    <- liftIO $ fmap isSymbolicLink $ getSymbolicLinkStatus absolutePath
        newAcc <- folder acc relativePath
        follow <- fmap sFollowSymlink get
        if isDir && (follow || not sym)
          then findFoldDirFilter folder newAcc dirFilter relativePath
          else return newAcc