{-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS -Wall #-} module Extra.IO ( -- * IO functions to write files and notice when they change. testAndWriteDotNew , testAndWriteBackup , testAndWriteFile , testAndWrite , writeFileWithBackup , findHaskellFiles , timeComputation ) where import Control.Exception as E (IOException, throw, try) import Control.Monad (when) import Control.Monad.Trans (liftIO, MonadIO) import Data.Monoid ((<>)) import Data.Text as Text (length, take, Text) import Data.Text.IO as Text (readFile, writeFile) import Data.Time (getCurrentTime, diffUTCTime, getCurrentTime, NominalDiffTime) --import Extra.Log (alog) import Extra.Text (diffText) import System.Directory (getCurrentDirectory, removeFile, renameFile) import System.FilePath.Find as Find ((==?), (&&?), always, extension, fileType, FileType(RegularFile), find) import System.IO.Error (isDoesNotExistError) import System.Log.Logger (logM, Priority(DEBUG, ERROR)) testAndWriteDotNew :: FilePath -> Text -> IO () testAndWriteDotNew dest new = testAndWrite writeDotNew dest new testAndWriteBackup :: FilePath -> Text -> IO () testAndWriteBackup dest new = testAndWrite (\dest' _ new' -> writeFileWithBackup dest' new') dest new testAndWriteFile :: FilePath -> Text -> IO () testAndWriteFile = testAndWriteDotNew {-# DEPRECATED testAndWriteFile "Use testAndWriteDotNew" #-} -- | See if the new Paths code matches the old, if not write it to a -- file with the suffix ".new" and throw an error so the new code can -- be inspected and checked in. If the new file does match, the -- existing .new file is removed. testAndWrite :: (FilePath -> Text -> Text -> IO ()) -> FilePath -> Text -> IO () testAndWrite changeAction dest new = do here <- getCurrentDirectory logM "Extra.IO" DEBUG ("testAndWriteFile " <> show dest <> " " <> show (shorten 50 new) <> " (cwd=" <> show here <> ")") removeFileMaybe (dest <> ".new") try (Text.readFile dest >>= \old -> when (old /= new) (changeAction dest old new)) >>= either (\(e :: IOException) -> case isDoesNotExistError e of True -> do logM "Extra.IO" DEBUG "testAndWriteFile - no existing version" Text.writeFile dest new False -> do logM "Extra.IO" ERROR ("testAndWriteFile " <> show dest <> " - IOException " ++ show e) throw e) return -- | Shorten a string to a maximum length by replacing its suffix with "..." shorten :: Int -> Text -> Text shorten n t | n <= 3 = Text.take n t -- no room for an ellipsis shorten n t | Text.length t > n - 3 = Text.take (n - 3) t <> "..." shorten _ t = t -- | If the new file does not match the old, write it to file.new and error. writeDotNew :: FilePath -> Text -> Text -> IO () writeDotNew dest old new = do logM "Extra.IO" DEBUG ("testAndWriteFile - mismatch, writing " <> show (dest <> ".new")) Text.writeFile (dest <> ".new") new error ("Generated " <> dest <> ".new does not match existing " <> dest <> ":\n" <> diffText (dest, old) (dest <> ".new", new) <> "\nIf these changes look reasonable move " <> dest <> ".new to " <> dest <> " and retry.") -- | Rename existing file with suffix "~" and write a new file writeFileWithBackup :: FilePath -> Text -> IO () writeFileWithBackup dest text = do removeFileMaybe (dest <> "~") renameFileMaybe dest (dest <> "~") removeFileMaybe dest Text.writeFile dest text -- | Remove a file if it exists removeFileMaybe :: FilePath -> IO () removeFileMaybe p = try (removeFile p) >>= either (\(e :: IOException) -> case isDoesNotExistError e of True -> pure () False -> throw e) pure -- | Rename a file if it exists renameFileMaybe :: FilePath -> FilePath -> IO () renameFileMaybe oldpath newpath = try (renameFile oldpath newpath) >>= either (\(e :: IOException) -> case isDoesNotExistError e of True -> pure () False -> throw e) pure -- | Find all regular files with extension .hs findHaskellFiles :: FilePath -> IO [FilePath] findHaskellFiles dir = find always (Find.extension ==? ".hs" &&? fileType ==? RegularFile) dir -- | Perform an IO operation and return the elapsed time along with the result. timeComputation :: MonadIO m => m r -> m (r, NominalDiffTime) timeComputation a = do !start <- liftIO getCurrentTime !r <- a !end <- liftIO getCurrentTime return (r, diffUTCTime end start)