【问题标题】:Filter a list of paths to only include files过滤路径列表以仅包含文件
【发布时间】:2015-10-03 20:18:16
【问题描述】:

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

例如,使用getDirectoryContents

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

其中“filterFunction”是一个仅返回代表文件的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 将仅包含 "."".."

【问题讨论】:

    标签: file haskell io path


    【解决方案1】:

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

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

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

    【讨论】:

      【解决方案2】:

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

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

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

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

      4. 您正在寻找FilePath -&gt; IO Bool 类型的函数。

      if we search for that on Hoogle,第一个结果是doesFileExist :: FilePath -&gt; IO Bool from System.Directory。来自文档:

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

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

      现在,你如何使用它?你不能在这里使用filter,因为你有一个有效的功能。你可以再次使用 Hoogle——如果 filter 的类型为 (a -&gt; Bool) -&gt; [a] -&gt; [a],那么使用 monad m 注释函数的结果会给你新的类型 Monad m =&gt; (a -&gt; m Bool) -&gt; [a] -&gt; m [Bool]——但有一个更简单的“廉价技巧”。一般来说,如果func 是一个具有有效/单子版本的函数,则该有效/单子版本称为funcM,它通常存在于Control.Monad。¹事实上,有一个函数Control.Monad.filterM :: Monad m =&gt; (a -&gt; m Bool) -&gt; [a] -&gt; m [a]

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

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

      因此,我们可以采取两种方法来解决问题。首先是调整getDirectoryContents 的结果,以便能够正确解释它们。 (我们还丢弃了... 结果,尽管如果您只是在寻找常规文件,它们不会有任何损害。)这将返回包含正在检查其内容的目录的文件名。调整getDirectoryContents 函数如下所示:

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

      filter 去掉了特殊目录,map 将参数目录添加到所有结果中。这使得返回的文件可以接受 doesFileExist 的参数。 (如果您以前没有见过它们,(System.FilePath.&lt;/&gt;) 会附加两个文件路径;而(Control.Applicative.&lt;$&gt;),也可用作(Data.Functor.&lt;$&gt;),是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
      

      或者,如果您喜欢花哨/无意义:

      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 在适当的当前目录下运行。这将只返回相对于正在检查其内容的目录的文件名。为此,我们要使用withCurrentDirectory :: FilePath -&gt; IO a -&gt; IO a 函数(但见下文),然后将getDirectoryContents 传递给当前目录"." 参数。 withCurrentDirectory 的文档(部分)说:

      使用给定的工作目录运行IO 操作并在之后恢复原始工作目录,即使给定的操作由于异常而失败。

      把所有这些放在一起,我们得到了以下代码

      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 -&gt; (a -&gt; IO b) -&gt; (a -&gt; IO c) -&gt; IO c 的形式实现。 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 "."
      

      另外,关于dos 中的lets 的简短说明:在do 块中,

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

      等价于

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

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


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

      【讨论】:

      • 这是一个很好的答案。打得很好。
      • 虽然我感谢您的回答和努力,但它似乎不起作用!试试我刚刚发布的编辑过的代码示例(或者如果我犯了错误,也许只是告诉我它有什么问题)。
      • @Langston:哦,天哪,我以前遇到过,但我忘了! getDirectoryContents 返回相对于其参数的路径; doesDirectoryExist 将路径视为相对于当前工作目录。对于那个很抱歉!我会在这个答案中添加代码来解决这个问题。
      • @Langston:通过几种不同的方式完成了 :-)(另外我正确地使用了 doesFileExist 而不是 doesDirectoryExist - 哎呀!)
      • map (fp &lt;/&gt;) . filter (`notElem` [".",".."]) &lt;$&gt; getDirectoryContents fp 可以压缩为map (fp &lt;/&gt;) &lt;$&gt; listDirectory fp。见System.Direcotry.listDirectory
      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2020-06-18
          • 2019-05-07
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多