如果我有一个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
此外,变量子目标只包含"."
和".."
。
答案 0 :(得分:18)
要查找标准库函数,Hoogle是一个很好的资源;它是一个Haskell搜索引擎,可让您按类型搜索 。使用它需要弄清楚如何考虑Haskell Way™的类型,但是你提出的类型签名并不适用。所以:
您正在寻找[Filepath] -> [Filepath]
。请记住,Haskell拼写是FilePath
。所以...
您正在寻找[FilePath] -> [FilePath]
。这是不必要的;如果你想过滤事物,你应该使用filter
。所以...
您正在寻找可以传递给FilePath -> Bool
的{{1}}类型的函数。但这可能不太正确:此函数需要查询文件系统,这是一种效果,而Haskell使用filter
跟踪类型系统中的效果。所以...
您正在寻找IO
类型的函数。
if we search for that on Hoogle,第一个结果是来自doesFileExist :: FilePath -> IO Bool
的System.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
; print
比Applicative
更多的东西(虽然这意味着你可以用它们做更少的事情)。在这种情况下,有效的功能可能存在于那里,或者也可能存在于Data.Foldable
或Data.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)