Отфильтровать список путей для включения только файлов

Если у меня есть список FilePaths, как я могу их фильтровать, чтобы возвращать только те, которые являются обычными файлами (а именно, не символическими ссылками или каталогами)?

Например, используя getDirectoryContents

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

где "filterFunction" - это функция, которая возвращает только FilePaths, которые представляют файлы.

Ответ может просто работать на Linux, но поддержка кросс-платформы предпочтительнее.

[EDIT] Просто использование didDirectoryExist не работает должным образом. Этот script печатает список всего в каталоге, а не только файлы:

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

Кроме того, переменные поддиры будут содержать только "." и "..".

Ответ 1

Чтобы найти стандартные библиотечные функции, Hoogle - отличный ресурс; это поисковая система Haskell, которая позволяет вам искать по типу. Использование этого требует выяснения того, как думать о типах Haskell Way ™, хотя, с которыми ваши предлагаемые типы подписей не совсем работают. Итак:

  • Вы ищете [Filepath] -> [Filepath]. Помните, что написание Haskell FilePath. Так что...

  • Вы ищете [Filepath] -> [Filepath]. Это не нужно; если вы хотите фильтровать вещи, вы должны использовать filter. Так что...

  • Вы ищете функцию типа FilePath -> Bool, которую вы можете передать в filter. Но это не совсем правильно: эта функция должна запрашивать файловую систему, которая является эффектом, а эффекты Haskell отслеживают в системе типов с помощью IO. Так что...

  • Вы ищете функцию типа FilePath -> IO Bool.

И если мы ищем это в Hoogle, первый результат doesFileExist :: FilePath -> IO Bool из System.Directory. Из документов:

Операция doesFileExist возвращает True если файл аргумента существует и не является каталогом, а False в противном случае.

Итак, System.Directory.doesFileExist - это именно то, что вы хотите. (Ну... только с небольшой дополнительной работой! См. Ниже.)

Теперь, как вы его используете? Вы не можете использовать filter здесь, потому что у вас есть эффективная функция. Вы можете снова использовать Hoogle - если filter имеет тип (a -> Bool) -> [a] -> [a], тогда аннотирование результатов функций с помощью монады 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, чтобы их можно было интерпретировать правильно. (Мы также отбрасываем результаты . и .., хотя, если вы просто ищете обычные файлы, они ничего не повредят.) Это вернет имена файлов, которые включают каталог, содержимое которого проверяется. Функция настройки getDirectoryContents выглядит следующим образом:

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

filter избавляется от специальных каталогов, а map добавляет каталог аргументов ко всем результатам. Это делает возвращаемые файлы приемлемыми аргументами для doesFileExist. (Если вы их раньше не видели, (System.FilePath.</>) добавляет два пути к файлу и (Control.Applicative.<$>), также доступный как (Data.Functor.<$>), является синонимом infix для 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 -> IO a -> IO a (но см. Ниже), а затем передать getDirectoryContents текущий каталог "." аргумент. В документации для withCurrentDirectory указано (частично):

Запустите IO действие с данным рабочим каталогом и затем восстановите исходный рабочий каталог, даже если данное действие завершилось неудачно из-за исключение.

Объединяя все это вместе, мы получаем следующий код

import Control.Monad
import System.Directory

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

Это то, что мы хотим, но, к сожалению, оно доступно только в версии 1.3.2.0 пакета directory - на момент написания этой статьи, самой последней, а не той, которая у меня есть. К счастью, это простая функция для реализации; такие функции 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

И затем используйте это, чтобы получить окончательный код:

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 в do s: в блоке do,

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

эквивалентно

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

Таким образом, ваш примерный код не нужен in в let и может вызывать вызов print.


¹ Не всегда: иногда вам нужны разные классы эффектов! Используйте Applicative из Control.Applicative, когда это возможно; больше вещей Applicative, чем Monad (хотя это означает, что вы можете сделать меньше с ними). В этом случае эффективные функции могут жить там, а также в Data.Foldable или Data.Traversable.

Ответ 2

Для систем Unix пакет unix предоставляет следующие API:

Вы можете использовать их для достижения желаемого. Пример демонстрации их использования в GHCI:

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

Ответ 3

Вы можете использовать библиотеку shelly. Он предназначен для создания сценариев оболочки с помощью Haskell. Вот решение с 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 - выполнить script:

shelly :: MonadIO m => Sh a -> m a

также мы используем fromString для создания скрытого FilePath. Существует выделенный тип, это не просто строка.

Ответ 4

Мне понадобился способ перечислить только обычные файлы в каталоге, и именно так я это делаю. Я подумал, что это может быть полезно:

import System.Directory

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