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

Filter a list of paths to only include files

如果我有一个 FilePaths 的列表,我如何才能将它们过滤为 return 只有那些是常规文件(即,不是符号链接或目录)的文件?

例如,使用getDirectoryContents

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

其中 "filterFunction" 是一个 return 仅表示文件的 FilePaths 的函数。

答案可能仅适用于 Linux,但最好支持跨平台。

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

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

此外,变量 subdirs 将只包含 "."".."

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

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

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

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

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

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

  3. 您正在寻找可以传递给 filterFilePath -> Bool 类型的函数。但这不太正确:此函数需要查询文件系统,这是一个效果,并且 Haskell 使用 IO 跟踪类型系统中的效果。所以……

  4. 您正在寻找类型为 FilePath -> IO Bool 的函数。

if we search for that on Hoogle, the first result is doesFileExist :: FilePath -> IO Bool from System.Directory。来自文档:

The operation doesFileExist returns True if the argument file exists and is not a directory, and False otherwise.

所以System.Directory.doesFileExist正是您想要的。 (好吧......只是做了一点额外的工作!见下文。)

现在,你如何使用它?你不能在这里使用filter,因为你有一个有效的功能。您可以再次使用 Hoogle – 如果 filter 具有类型 (a -> Bool) -> [a] -> [a],则使用 monad m 注释函数的结果将为您提供新类型 Monad m => (a -> m Bool) -> [a] -> m [Bool] – but there's an easier "cheap trick". In general, if func is a function with an effectful/monadic version, that effectful/monadic version is called funcM, and it often lives in Control.Monad.¹ And indeed, there is a function Control.Monad.filterM :: Monad m => (a -> m Bool) -> [a] -> m [a] .

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

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

因此,我们可以采用两种方法来解决问题。首先是调整 getDirectoryContents 的结果,以便它们可以被正确解释。 (我们也丢弃了 ... 结果,尽管如果您只是寻找常规文件,它们不会造成任何伤害。)这将 return 文件名,其中包括目录内容正在审核中。调整 getDirectoryContents 函数如下所示:

getQualifiedDirectoryContents :: FilePath -> IO [FilePath]
getQualifiedDirectoryContents fp =
    map (fp </>) . filter (`notElem` [".",".."]) <$> getDirectoryContents fp

filter 删除了特殊目录,map 将参数目录添加到所有结果中。这使得 returned 文件可以接受 doesFileExist 的参数。 (如果您以前没有见过它们,(System.FilePath.</>) appends two file paths; and (Control.Applicative.<$>), also available as (Data.Functor.<$>), is an infix synonym for fmap, which is like liftM 但适用范围更广。)

将所有这些放在一起,您的最终代码变为:

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

或者,如果您想成为 fancy/point-free:

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"

第二种方法是进行调整,使 doesFileExist 运行 具有适当的当前目录。这将 return 只是相对于正在检查其内容的目录的文件名。为此,我们要使用 withCurrentDirectory :: FilePath -> IO a -> IO a 函数(见下文),然后将 getDirectoryContents 当前目录传递给 "." 参数。 withCurrentDirectory 的文档说(部分):

Run an IO action with the given working directory and restore the original working directory afterwards, even if the given action fails due to an exception.

将所有这些放在一起得到以下代码

import Control.Monad
import System.Directory

main :: IO ()
main = withCurrentDirectory "/foo/bar" $
         print =<< filterM doesFileExist =<< getDirectoryContents "."

这就是我们想要的,但不幸的是,它仅在 directory 包的 1.3.2.0 版本中可用——截至撰写本文时,这是最新的版本,而不是我拥有的版本。幸运的是,这是一个很容易实现的功能;这种局部设置值函数通常根据 Control.Exception.bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c 来实现。 bracket 函数是 运行 as 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 "."

此外,关于 dos 中的 lets 的快速说明:在 do 块中,

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

等同于

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

因此您的示例代码不需要 let 中的 in 并且可以缩进 print 调用。


¹ 并非总是如此:有时您需要不同的 类 效果!使用 Applicative from Control.Applicative when possible; more things are Applicatives than are Monads (although this means you can do less with them). In that case, the effectful functions may live there, or also in Data.Foldable or Data.Traversable.

您可以使用库 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 的文件路径。有一个专用的类型,它不仅仅是一个字符串。

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

import System.Directory

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