Reputation: 2476
I am trying to read the contents of a file, turn the text to upper-case and then write it back.
Here is the code I had written:
import System.IO
import Data.Char
main = do
handle <- openFile "file.txt" ReadWriteMode
contents <- hGetContents handle
hClose handle
writeFile "file.txt" (map toUpper contents)
return ()
However, this writes nothing to the file, in fact, it even clears it.
I made some changes:
main = do
handle <- openFile "file.txt" ReadWriteMode
contents <- hGetContents handle
writeFile "file.txt" (map toUpper contents)
hClose handle
return ()
However, I get the error resource busy (file is locked)
. How can I get this working and why it didn't work in both cases?
Upvotes: 12
Views: 25576
Reputation: 12898
@bwroga's answer is perfectly correct. Here is an implementation of the suggested approach (writing to temporary file & renaming):
import Data.Char (toUpper)
import System.Directory (renameFile, getTemporaryDirectory)
import System.Environment (getArgs)
main = do
[file] <- getArgs
tmpDir <- getTemporaryDirectory
let tmpFile = tmpDir ++ "/" ++ file
readFile file >>= writeFile tmpFile . map toUpper
renameFile tmpFile file
Upvotes: 5
Reputation: 5459
I think your problem is that hGetContents is lazy. The contents of the file are not immediately read in when you use hGetContents. They are read in when they are needed.
In your first example you open the file and say that you want the contents, but then close the file before you do anything with them. Then you write to the file. When you write to an existing file the contents are cleared, but since you closed the file you no longer have access to the file contents.
In the second example you open the file and then try to write to the file, but because the contents are not really read until they are needed (when they are transformed and written back) you end up trying to write to and read from the same file at the same time.
You could write to a file named file2.txt then when you are done, delete file.txt and rename file2.txt to file.txt
Upvotes: 6
Reputation: 89063
There's a couple things going on here
You opened the file in ReadWriteMode
, but only read the contents. Why not use the same handle for both?
main = do
handle <- openFile "file.txt" ReadWriteMode
contents <- hGetContents' handle
hSeek handle AbsoluteSeek 0
hPutStr handle (map toUpper contents)
hClose handle
return ()
hGetContents
will put the handle in a semi-closed state, so you'll need something else to read the file contents:
hGetContents' :: Handle -> IO String
hGetContents' h = do
eof <- hIsEOF h
if eof
then
return []
else do
c <- hGetChar h
fmap (c:) $ hGetContents' h
Upvotes: 5
Reputation: 54058
Lazy IO is bad, and this is generally considered to be a pain point in Haskell. Basically the contents
isn't evaluated until you go to write it back to disk, at which point it can't be evaluated because the file is already closed. You can fix this in several ways, without resorting to extra libraries you can use the readFile
function and then check the length before writing back out:
import Control.Monad (when)
main = do
contents <- readFile "file.txt"
let newContents = map toUpper contents
when (length newContents > 0) $
writeFile "file.txt" newContents
I would say this code is actually better anyway because you don't write back out to a file that is already empty, a pointless operation.
Another way would be to use a streaming library, pipes
is a popular choice with some good tutorials and a solid mathematical foundation, and that would be my choice as well.
Upvotes: 12