17

I find myself doing more and more scripting in haskell. But there are some cases where I'm really not sure of how to do it "right".
e.g. copy a directory recursively (a la unix cp -r).

Since I mostly use linux and Mac Os I usually cheat:

import System.Cmd
import System.Exit

copyDir ::  FilePath -> FilePath -> IO ExitCode
copyDir src dest = system $ "cp -r " ++ src ++ " " ++ dest

But what is the recommended way to copy a directory in a platform independent fashion?
I didn't find anything suitable on hackage.

This is my rather naiv implementation I use so far:

import System.Directory
import System.FilePath((</>))
import Control.Applicative((<$>))
import Control.Exception(throw)
import Control.Monad(when,forM_)

copyDir ::  FilePath -> FilePath -> IO ()
copyDir src dst = do
  whenM (not <$> doesDirectoryExist src) $
    throw (userError "source does not exist")
  whenM (doesFileOrDirectoryExist dst) $
    throw (userError "destination already exists")

  createDirectory dst
  content <- getDirectoryContents src
  let xs = filter (`notElem` [".", ".."]) content
  forM_ xs $ \name -> do
    let srcPath = src </> name
    let dstPath = dst </> name
    isDirectory <- doesDirectoryExist srcPath
    if isDirectory
      then copyDir srcPath dstPath
      else copyFile srcPath dstPath

  where
    doesFileOrDirectoryExist x = orM [doesDirectoryExist x, doesFileExist x]
    orM xs = or <$> sequence xs
    whenM s r = s >>= flip when r

Any suggestions of what really is the way to do it?


I updated this with the suggestions of hammar and FUZxxl.
...but still it feels kind of clumsy to me for such a common task!

Uli Köhler
  • 13,012
  • 16
  • 70
  • 120
oliver
  • 9,235
  • 4
  • 34
  • 39

6 Answers6

5

It's possible to use the Shelly library in order to do this, see cp_r:

cp_r "sourcedir" "targetdir"

Shelly first tries to use native cp -r if available. If not, it falls back to a native Haskell IO implementation.

For further details on type semantics of cp_r, see this post written by me to described how to use cp_r with String and or Text.

Shelly is not platform independent, since it relies on the Unix package, which is not supported under Windows.

Uli Köhler
  • 13,012
  • 16
  • 70
  • 120
  • As of today, Shelly's cp_r is buggy.`cp_r "A" "A/B"` loops see, https://github.com/yesodweb/Shelly.hs/issues/154 – Andreas Abel Aug 18 '17 at 16:36
  • @andreas.abel Thanks for the notice. As I've answered on GitHub, I don't think it's really broken, except in a very special corner case that results in an error mesage using native `cp -r`. I think it should be fixed in Shelly, but even if it isn't fixed I think this is still the most pragmatic solution to the original question for most users. – Uli Köhler Aug 19 '17 at 02:43
4

I couldn't find anything that does this on Hackage.

Your code looks pretty good to me. Some comments:

  1. dstExists <- doesDirectoryExist dst
    

    This does not take into account that a file with the destination name might exist.

  2. if or [not srcExists, dstExists] then print "cannot copy"
    

    You might want to throw an exception or return a status instead of printing directly from this function.

  3. paths <- forM xs $ \name -> do
        [...]
      return ()
    

    Since you're not using paths for anything, you can change this to

    forM_ xs $ \name -> do
      [...]
    
hammar
  • 138,522
  • 17
  • 304
  • 385
  • You might also consider writing `if not srcExists || dstExists then print "cannot copy"` – fuz Jul 24 '11 at 16:45
  • hammar and @FUXxxl: thanks for your suggestions. I updated the code! ...wish this would be packaged in some neat library. – oliver Jul 24 '11 at 18:50
  • @oliver: The `return ()` at the end is now redundant. – hammar Jul 24 '11 at 18:54
  • @oliver: Also `error` is generally used for things that should be considered programming errors, such as calling `head` on an empty list. For IO related errors an IO exception should be used instead. See: [Error vs. Exception](http://www.haskell.org/haskellwiki/Error_vs._Exception). – hammar Jul 24 '11 at 19:36
  • @hammar: thanks for your insights...the article you cite was interesting. I updated my version with this...not sure if this is the correct way to us a `userError` here ... but it is catchable. This was the only way I know of how to throw an IOException. – oliver Jul 26 '11 at 09:19
2

The filesystem-trees package provides the means for a very simple implementation:

import System.File.Tree (getDirectory, copyTo_)

copyDirectory :: FilePath -> FilePath -> IO ()
copyDirectory source target = getDirectory source >>= copyTo_ target
Johannes Gerer
  • 25,508
  • 5
  • 29
  • 35
  • 1
    Great. But looking today (Aug 2017), the last commit on this package is from 2015, and the hackage build matrix is empty. – Andreas Abel Aug 18 '17 at 16:04
1

I assume that the function in Path.IO copyDirRecur with variants to include/exclude symlinks may be a newer and maintained solution. It requires to convert the filepath to Path x Dir which is achieved with parseRelDir respective parseAbsDir, but I think to have a more precise date type than FilePath is worthwile to avoid hard to track errors at run-time.

user855443
  • 2,596
  • 3
  • 25
  • 37
  • `path` and `path-io` are definitely a very good option to consider before rolling your own. Mind that `copyDirRecur` does not behave *exactly* like `cp -r`. – user2847643 May 12 '19 at 06:40
1

The MissingH package provides recursive directory traversals, which you might be able to use to simplify your code.

Uli Köhler
  • 13,012
  • 16
  • 70
  • 120
kowey
  • 1,211
  • 7
  • 15
0

There are also some functions for copying files and directories in the core Haskell library Cabal modules, specifically Distribution.Simple.Utils in package Cabal. copyDirectoryRecursive is one, and there are other functions near this one in that module.

dino
  • 1,123
  • 9
  • 13