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

Lan*_*ton 11 io haskell file path

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

例如,使用 getDirectoryContents

main = do
    contents <- getDirectoryContents "/foo/bar"
    let onlyFiles = filterFunction contents in
        print onlyFiles
Run Code Online (Sandbox Code Playgroud)

其中"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
Run Code Online (Sandbox Code Playgroud)

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

Ant*_*sky 19

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

  1. 你在找[Filepath] -> [Filepath].记住,Haskell拼写是FilePath.所以…

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

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

  4. 你正在寻找一种类型的功能FilePath -> IO Bool.

而且,如果我们搜索上Hoogle,第一个结果是doesFileExist :: FilePath -> IO BoolSystem.Directory.来自文档:

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

所以System.Directory.doesFileExist正是你想要的.(嗯......只需要一点额外的工作!见下文.)

现在,你如何使用它?你不能filter在这里使用,因为你有一个有效的功能.你可以再次使用Hoogle - 如果filter有类型(a -> Bool) -> [a] -> [a],然后用monad注释函数的结果m给你新的类型Monad m => (a -> m Bool) -> [a] -> m [Bool]- 但是有一个更容易的"廉价技巧".通常,如果func是一个具有有效/一元版本的函数,则会调用该有效/一元版本funcM,并且它通常存在Control.Monad.¹实际上,还有一个函数Control.Monad.filterM :: Monad m => (a -> m Bool) -> [a] -> m [a].

然而! 就像我们不愿承认它一样,即使在Haskell中,类型也不能提供您需要的所有信息.重要的是,我们在这里遇到问题:

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

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

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

filter摆脱了特殊的目录,并map预先将参数目录所有的结果.这使返回的文件可以接受参数doesFileExist.(如果您之前没有看过它们,请(System.FilePath.</>)附加两个文件路径;并且(Control.Applicative.<$>),也可以作为中(Data.Functor.<$>)缀的同义词fmap,这类似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
Run Code Online (Sandbox Code Playgroud)

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

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"
Run Code Online (Sandbox Code Playgroud)

第二种方法是调整事物,以便doesFileExist使用适当的当前目录运行.这将仅返回相对于正在检查其内容的目录的文件名.为此,我们希望使用该withCurrentDirectory :: FilePath -> IO a -> IO a函数(但见下文),然后传递getDirectoryContents当前目录"."参数.文件withCurrentDirectory说(部分):

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

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

import Control.Monad
import System.Directory

main :: IO ()
main = withCurrentDirectory "/foo/bar" $
         print =<< filterM doesFileExist =<< getDirectoryContents "."
Run Code Online (Sandbox Code Playgroud)

这就是我们想要的,但不幸的是,它只能在directory包的1.3.2.0版本中使用- 在撰写本文时,是最新版本,而不是我拥有的版本.幸运的是,这是一个很容易实现的功能; 这样的set-a-value-local函数通常用来实现Control.Exception.bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c.该bracket函数运行为bracket before after action,并正确处理异常.所以我们可以定义withCurrentDirectory自己:

withCurrentDirectory :: FilePath -> IO a -> IO a
withCurrentDirectory fp m =
  bracket getCurrentDirectory setCurrentDirectory $ \_ -> do
    setCurrentDirectory fp
    m
Run Code Online (Sandbox Code Playgroud)

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

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 "."
Run Code Online (Sandbox Code Playgroud)

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

do ...foo...
   let x = ...bar...
   ...baz...
Run Code Online (Sandbox Code Playgroud)

相当于

do ...foo...
   let x = ...bar... in
     do ...baz...
Run Code Online (Sandbox Code Playgroud)

因此,您的示例代码不需要inin let并且可以使print调用更加突出.


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


Sib*_*ibi 5

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

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

?> import System.Posix.Files
?> status <- getFileStatus "/home/sibi"
?> isDirectory status
True
?> isRegularFile status
False
Run Code Online (Sandbox Code Playgroud)