过滤路径列表以仅包含文件
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™ 的方式思考类型,但是,您建议的类型签名不太适用。所以:
您正在寻找 [Filepath] -> [Filepath]
。请记住,Haskell 的拼写是 FilePath
。所以……
您正在寻找 [FilePath] -> [FilePath]
。这是不必要的;如果你想过滤东西,你应该使用 filter
。所以……
您正在寻找可以传递给 filter
的 FilePath -> Bool
类型的函数。但这不太正确:此函数需要查询文件系统,这是一个效果,并且 Haskell 使用 IO
跟踪类型系统中的效果。所以……
您正在寻找类型为 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 "."
此外,关于 do
s 中的 let
s 的快速说明:在 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 Applicative
s than are Monad
s (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)
如果我有一个 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™ 的方式思考类型,但是,您建议的类型签名不太适用。所以:
您正在寻找
[Filepath] -> [Filepath]
。请记住,Haskell 的拼写是FilePath
。所以……您正在寻找
[FilePath] -> [FilePath]
。这是不必要的;如果你想过滤东西,你应该使用filter
。所以……您正在寻找可以传递给
filter
的FilePath -> Bool
类型的函数。但这不太正确:此函数需要查询文件系统,这是一个效果,并且 Haskell 使用IO
跟踪类型系统中的效果。所以……您正在寻找类型为
FilePath -> IO Bool
的函数。
和if we search for that on Hoogle, the first result is doesFileExist :: FilePath -> IO Bool
from System.Directory
。来自文档:
The operation
doesFileExist
returnsTrue
if the argument file exists and is not a directory, andFalse
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 "."
此外,关于 do
s 中的 let
s 的快速说明:在 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 Applicative
s than are Monad
s (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)