过滤仅包含文件的路径列表

时间:2015-07-15 00:09:58

标签: file haskell io path

如果我有一个FilePaths列表,如何过滤它们只返回常规文件(即不是符号链接或目录)?

例如,使用getDirectoryContents

main = do
    contents <- getDirectoryContents "/foo/bar"
    let onlyFiles = filterFunction contents in
        print onlyFiles

其中“filterFunction”是仅返回表示文件的FilePaths的函数。

答案可能适用于Linux,但首选跨平台支持。

[编辑]只是使用didDirectoryExist不能按预期工作。此脚本打印目录中所有内容的列表,而不仅仅是文件:

module Main where

import System.Directory
import Control.Monad (filterM, liftM)

getFiles :: FilePath -> IO [FilePath]
getFiles root = do
    contents <- getDirectoryContents root
    filesHere <- filterM (liftM not . doesDirectoryExist) contents
    subdirs <- filterM doesDirectoryExist contents
    return filesHere

main = do
    files <- getFiles "/"
    print $ files

此外,变量子目标只包含"."".."

4 个答案:

答案 0 :(得分:18)

要查找标准库函数,Hoogle是一个很好的资源;它是一个Haskell搜索引擎,可让您按类型搜索 。使用它需要弄清楚如何考虑Haskell Way™的类型,但是你提出的类型签名并不适用。所以:

  1. 您正在寻找[Filepath] -> [Filepath]。请记住,Haskell拼写是FilePath。所以...

  2. 您正在寻找[FilePath] -> [FilePath]。这是不必要的;如果你想过滤事物,你应该使用filter。所以...

  3. 您正在寻找可以传递给FilePath -> Bool的{​​{1}}类型的函数。但这可能不太正确:此函数需要查询文件系统,这是一种效果,而Haskell使用filter跟踪类型系统中的效果。所以...

  4. 您正在寻找IO类型的函数。

  5. if we search for that on Hoogle,第一个结果是来自doesFileExist :: FilePath -> IO BoolSystem.Directory。来自文档:

      

    如果参数文件存在且不是目录,则操作doesFileExist返回True,否则返回False

    所以FilePath -> IO Bool正是你想要的。 (嗯......只需要一点额外的工作!见下文。)

    现在,你如何使用它?你不能在这里使用System.Directory.doesFileExist,因为你有一个有效的功能。您可以再次使用Hoogle - 如果filter的类型为filter,则使用monad (a -> Bool) -> [a] -> [a]注释函数的结果会为您提供新类型Monad m => (a -> m Bool) -> [a] -> m [Bool] - 但是有一个更容易“廉价的技巧”。通常,如果m是具有有效/一元版本的函数,则该有效/一元版本称为func,它通常存在于Control.Monad。¹实际上,有一个函数Control.Monad.filterM :: Monad m => (a -> m Bool) -> [a] -> m [a]

    然而!尽管我们不愿意承认它,但即使在Haskell中,类型也不能提供您需要的所有信息。重要的是,我们将在这里遇到问题:

    • 作为函数参数给出的文件路径是相对于当前目录解释,但是......
    • getDirectoryContents返回路径相对于其参数

    因此,我们可以采取两种方法来解决问题。第一个是调整funcM的结果,以便正确解释它们。 (我们也放弃getDirectoryContents.结果,但如果您只是寻找常规文件,它们不会伤害任何内容。)这将返回包含正在检查其内容的目录的文件名。 。 adjust ..函数如下所示:

    getDirectoryContents

    getQualifiedDirectoryContents :: FilePath -> IO [FilePath] getQualifiedDirectoryContents fp = map (fp </>) . filter (`notElem` [".",".."]) <$> getDirectoryContents fp 删除了特殊目录,filter将参数目录添加到所有结果之前。这使返回的文件成为map的可接受参数。 (如果您之前没有看到它们,(System.FilePath.</>)会附加两个文件路径;而(Control.Applicative.<$>)也可以(Data.Functor.<$>)提供,它是fmap的中缀同义词,就像liftM但更广泛适用。)

    总而言之,您的最终代码变为:

    doesFileExist

    或者,如果您想要花哨/无点:

    import Control.Applicative
    import Control.Monad
    import System.FilePath
    import System.Directory
    
    getQualifiedDirectoryContents :: FilePath -> IO [FilePath]
    getQualifiedDirectoryContents fp =
        map (fp </>) . filter (`notElem` [".",".."]) <$> getDirectoryContents fp
    
    main :: IO ()
    main = do
      contents  <- getQualifiedDirectoryContents "/foo/bar"
      onlyFiles <- filterM doesFileExist contents
      print onlyFiles
    

    第二种方法是调整内容,以便import Control.Applicative import Control.Monad import System.FilePath import System.Directory getQualifiedDirectoryContents :: FilePath -> IO [FilePath] getQualifiedDirectoryContents fp = map (fp </>) . filter (`notElem` [".",".."]) <$> getDirectoryContents fp main :: IO () main = print =<< filterM doesFileExist =<< getQualifiedDirectoryContents "/foo/bar" 与适当的当前目录一起运行。这将仅返回相对于正在检查其内容的目录的文件名。为此,我们希望使用withCurrentDirectory :: FilePath -> IO a -> IO a函数(但请参见下文),然后将doesFileExist传递给当前目录getDirectoryContents参数。 "."的文档(部分)说明:

      

    使用给定的工作目录运行IO操作,然后恢复原始工作目录,即使给定的操作因异常而失败。

    将所有这些放在一起为我们提供了以下代码

    withCurrentDirectory

    这就是我们想要的,但不幸的是,它仅在import Control.Monad import System.Directory main :: IO () main = withCurrentDirectory "/foo/bar" $ print =<< filterM doesFileExist =<< getDirectoryContents "." 软件包的1.3.2.0版本中可用 - 截至撰写本文时,是最新版本,而不是我拥有的版本。幸运的是,这是一个很容易实现的功能;这种set-a-value-local函数通常用Control.Exception.bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c来实现。 directory函数作为bracket运行,并正确处理异常。所以我们可以自己定义bracket before after action

    withCurrentDirectory

    然后使用它来获取最终代码:

    withCurrentDirectory :: FilePath -> IO a -> IO a
    withCurrentDirectory fp m =
      bracket getCurrentDirectory setCurrentDirectory $ \_ -> do
        setCurrentDirectory fp
        m
    

    另外,关于import Control.Exception import Control.Monad import System.Directory withCurrentDirectory :: FilePath -> IO a -> IO a withCurrentDirectory fp m = bracket getCurrentDirectory setCurrentDirectory $ \_ -> do setCurrentDirectory fp m main :: IO () main = withCurrentDirectory "/foo/bar" $ print =<< filterM doesFileExist =<< getDirectoryContents "." let的{​​{1}}的一个快速说明:在do块中,

    do

    相当于

    do ...foo...
       let x = ...bar...
       ...baz...
    

    因此,您的示例代码不需要do ...foo... let x = ...bar... in do ...baz... 中的in,并且可以超越let来电。

    ¹并非总是如此:有时您需要不同类别的效果!尽可能使用Applicative中的Control.Applicative; printApplicative更多的东西(虽然这意味着你可以用它们做更少的事情)。在这种情况下,有效的功能可能存在于那里,或者也可能存在于Data.FoldableData.Traversable中。

答案 1 :(得分:5)

对于Unix系统,包unix公开了这些API:

您可以结合使用它们来实现您想要的效果。在GHCI中使用它们的示例演示:

λ> import System.Posix.Files
λ> status <- getFileStatus "/home/sibi"
λ> isDirectory status
True
λ> isRegularFile status
False

答案 2 :(得分:0)

您可以使用库shelly。它致力于使用Haskell进行shell脚本编写。以下是shelly的解决方案:

module Sh where

import Control.Monad
import Data.String 

import Shelly

dir = fromString "/home/me"

printAll = mapM_ print

main = do
    files <- shelly $ filterM test_f =<< ls dir
    printAll files

我们使用以下功能:

ls - 用于列出目录内容。

ls :: FilePath -> Sh [FilePath]

test_f - 用于测试目录是否为文件:

test_f :: FilePath -> Sh Bool

shelly - 执行脚本:

shelly :: MonadIO m => Sh a -> m a

我们也使用fromString来创建一个shelly的FilePath。有一种专用类型,它不仅仅是一个字符串。

答案 3 :(得分:0)

我碰巧需要一种方法来列出目录中的常规文件,这就是我的工作方式。我认为这可能会有所帮助:

import System.Directory

listFilesInDirectory :: FilePath -> IO [FilePath]
listFilesInDirectory dir = do
    rawList <- listDirectory dir
    filterM doesFileExist (map (dir </>) rawList)