{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE RankNTypes #-} module Highlight.Common.Monad.Input ( FileOrigin(..) , FileReader(..) , getFileOriginFromFileReader , getFilePathFromFileReader , InputData(..) , createInputData , FilenameHandlingFromFiles(..) ) where import Prelude () import Prelude.Compat import Control.Exception (IOException, try) import Control.Monad.IO.Class (MonadIO(liftIO)) import Data.ByteString (ByteString) import Data.List (sort) import Pipes (Pipe, Producer, Producer', Proxy, (>->), await, each, for, next, yield) import Pipes.Prelude (toListM) import qualified Pipes.Prelude as Pipes import System.IO (Handle) import Highlight.Common.Options (InputFilename(unInputFilename), Recursive(Recursive)) import Highlight.Pipes (childOf, fromHandleLines) import Highlight.Util (combineApplicatives, openFilePathForReading) ----------- -- Pipes -- ----------- -- | Place where a file originally came from. data FileOrigin = FileSpecifiedByUser FilePath -- ^ File was specified on the command line by the user. | FileFoundRecursively FilePath -- ^ File was found recursively (not directly specified by the user). | Stdin -- ^ Standard input. It was either specified on the command line as @-@, or -- used as default because the user did not specify any files. deriving (Eq, Read, Show) -- | Get a 'FilePath' from a 'FileOrigin'. -- -- >>> getFilePathFromFileOrigin $ FileSpecifiedByUser "hello.txt" -- Just "hello.txt" -- >>> getFilePathFromFileOrigin $ FileFoundRecursively "bye.txt" -- Just "bye.txt" -- >>> getFilePathFromFileOrigin Stdin -- Nothing getFilePathFromFileOrigin :: FileOrigin -> Maybe FilePath getFilePathFromFileOrigin (FileSpecifiedByUser fp) = Just fp getFilePathFromFileOrigin (FileFoundRecursively fp) = Just fp getFilePathFromFileOrigin Stdin = Nothing -- | This is used in two different places. -- -- One is in 'fileListProducer', where @a@ becomes 'Handle'. This represents a -- single file that has been opened. 'FileReaderSuccess' contains the -- 'FileOrigin' and the 'Handle'. 'FileReaderErr' contains the 'FileOrigin' -- and any errors that occurred when trying to open the 'Handle'. -- -- The other is in 'fileReaderHandleToLine' and 'InputData', where @a@ becomes -- 'ByteString'. This represents a single 'ByteString' line from a file, or an -- error that occurred when trying to read the file. -- -- 'FileReader' is usually wrapped in a 'Producer'. This is a stream of either -- 'Handle's or 'ByteString' lines (with any errors that have occurred). data FileReader a = FileReaderSuccess !FileOrigin !a | FileReaderErr !FileOrigin !IOException !(Maybe IOException) deriving (Eq, Show) -- | Get a 'FileOrigin' from a 'FileReader'. -- -- >>> let fileOrigin1 = FileSpecifiedByUser "hello.txt" -- >>> let fileReader1 = FileReaderSuccess fileOrigin1 "some line" -- >>> getFileOriginFromFileReader fileReader1 -- FileSpecifiedByUser "hello.txt" -- -- >>> let fileOrigin2 = FileFoundRecursively "bye.txt" -- >>> let fileReader2 = FileReaderErr fileOrigin2 (userError "err") Nothing -- >>> getFileOriginFromFileReader fileReader2 -- FileFoundRecursively "bye.txt" getFileOriginFromFileReader :: FileReader a -> FileOrigin getFileOriginFromFileReader (FileReaderSuccess origin _) = origin getFileOriginFromFileReader (FileReaderErr origin _ _) = origin -- | This is just -- @'getFilePathFromFileOrigin' '.' 'getFileOriginFromFileReader'@. -- -- >>> let fileOrigin1 = Stdin -- >>> let fileReader1 = FileReaderSuccess fileOrigin1 "some line" -- >>> getFilePathFromFileReader fileReader1 -- Nothing -- -- >>> let fileOrigin2 = FileFoundRecursively "bye.txt" -- >>> let fileReader2 = FileReaderErr fileOrigin2 (userError "err") Nothing -- >>> getFilePathFromFileReader fileReader2 -- Just "bye.txt" getFilePathFromFileReader :: FileReader a -> Maybe FilePath getFilePathFromFileReader = getFilePathFromFileOrigin . getFileOriginFromFileReader -- | This wraps up two pieces of information. -- -- One is the value of 'FilenameHandlingFromFiles'. This signals as to whether -- or not we need to print the filename when printing each line of output. -- -- This other is a 'Producer' of 'FileReader' 'ByteString's. This is a -- 'Producer' for each line of each input file. -- -- The main job of this module is to define 'createInputData', which produces -- 'InputData'. 'InputData' is what is processed to figure out what to output. data InputData m a = InputData !FilenameHandlingFromFiles !(Producer (FileReader ByteString) m a) -- | Create 'InputData' based 'InputFilename' list. -- -- Setup for examples: -- -- >>> :set -XOverloadedStrings -- >>> import Highlight.Common.Options (InputFilename(InputFilename)) -- >>> import Highlight.Common.Options (Recursive(NotRecursive)) -- -- If the 'InputFilename' list is empty, just create an 'InputData' with -- 'NoFilename' and the standard input 'Producer' passed in. -- -- >>> let stdinProd = yield ("hello" :: ByteString) -- >>> let create = createInputData NotRecursive [] stdinProd -- >>> InputData NoFilename prod <- create -- >>> toListM prod -- [FileReaderSuccess Stdin "hello"] -- -- If the 'InputFilename' list is not empty, create an 'InputData' with lines -- from each file found on the command line. -- -- >>> let inFiles = [InputFilename "test/golden/test-files/file1"] -- >>> let create = createInputData NotRecursive inFiles stdinProd -- >>> InputData NoFilename prod <- create -- >>> Pipes.head prod -- Just (FileReaderSuccess (FileSpecifiedByUser "test/golden/test-files/file1") "The...") createInputData :: forall m. MonadIO m => Recursive -- ^ Whether or not to recursively read in files. -> [InputFilename] -- ^ List of files passed in on the command line. -> Producer ByteString m () -- ^ A producer for standard input -> m (InputData m ()) createInputData recursive inputFilenames stdinProducer = do let fileOrigins = FileSpecifiedByUser . unInputFilename <$> inputFilenames case fileOrigins of [] -> return $ InputData NoFilename (stdinProducerToFileReader stdinProducer) _ -> do let fileListProducers = fmap (fileListProducer recursive) fileOrigins fileProducer = foldl1 combineApplicatives fileListProducers (filenameHandling, newFileProducer) <- computeFilenameHandlingFromFiles fileProducer let fileLineProducer = fileReaderHandleToLine newFileProducer return $ InputData filenameHandling fileLineProducer -- | Change a given 'Producer' into a 'FileReader' 'Producer' with the -- 'FileOrigin' set to 'Stdin'. -- -- You can think of this function as having the following type: -- -- @ -- 'stdinProducerToFileReader' -- :: 'Monad' m -- => 'Producer' a m r -- -> 'Producer' ('FileReader' a) m r -- @ -- -- >>> Pipes.head . stdinProducerToFileReader $ yield "hello" -- Just (FileReaderSuccess Stdin "hello") stdinProducerToFileReader :: forall x' x a m r. Monad m => Proxy x' x () a m r -> Proxy x' x () (FileReader a) m r stdinProducerToFileReader producer = producer >-> Pipes.map go where go :: a -> FileReader a go = FileReaderSuccess Stdin {-# INLINABLE stdinProducerToFileReader #-} -- | Convert a 'Producer' of 'FileReader' 'Handle' into a 'Producer' of -- 'FileReader' 'ByteString', where each line from the 'Handle' is 'yield'ed. -- -- You can think of this function as having the following type: -- -- @ -- 'fileReaderHandleToLine' -- :: 'MonadIO' m -- => 'Producer' ('FileReader' 'Handle') m r -- -> 'Producer' ('FileReader' 'ByteString') m r -- @ -- -- >>> let fileOrigin = FileSpecifiedByUser "test/golden/test-files/file2" -- >>> let producer = fileListProducer Recursive fileOrigin -- >>> Pipes.head $ fileReaderHandleToLine producer -- Just (FileReaderSuccess (FileSpecifiedByUser "test/golden/test-files/file2") "Pr... fileReaderHandleToLine :: forall m x' x r. MonadIO m => Proxy x' x () (FileReader Handle) m r -> Proxy x' x () (FileReader ByteString) m r fileReaderHandleToLine producer = producer >-> pipe where pipe :: Pipe (FileReader Handle) (FileReader ByteString) m r pipe = do fileReaderHandle <- await case fileReaderHandle of FileReaderErr fileOrigin fileErr dirErr -> yield $ FileReaderErr fileOrigin fileErr dirErr FileReaderSuccess fileOrigin handle -> do let linesProducer = fromHandleLines handle linesProducer >-> Pipes.map (FileReaderSuccess fileOrigin) pipe -- | Create a 'Producer' of 'FileReader' 'Handle' for a given 'FileOrigin'. -- -- Setup for examples: -- -- >>> import Highlight.Common.Options (Recursive(NotRecursive, Recursive)) -- -- If 'NoRecursive' is specified, just try to read 'FileOrigin' as a file. -- -- >>> let fileOrigin1 = FileSpecifiedByUser "test/golden/test-files/file2" -- >>> toListM $ fileListProducer NotRecursive fileOrigin1 -- [FileReaderSuccess (FileSpecifiedByUser "test/.../file2") {handle: test/.../file2}] -- -- If the file cannot be read, return an error. -- -- >>> let fileOrigin2 = FileSpecifiedByUser "thisfiledoesnotexist" -- >>> toListM $ fileListProducer NotRecursive fileOrigin2 -- [FileReaderErr (FileSpecifiedByUser "thisfiledoesnotexist") thisfiledoesnotexist: openBinaryFile: does not exist (No such file or directory) Nothing] -- -- If 'Recursive' is specified, then try to read 'FileOrigin' as a directory'. -- -- >>> let fileOrigin3 = FileSpecifiedByUser "test/golden/test-files/dir2" -- >>> toListM $ fileListProducer Recursive fileOrigin3 -- [FileReaderSuccess (FileFoundRecursively "test/.../dir2/file6") {handle: test/.../dir2/file6}] -- -- If the directory cannot be read, return an error. -- -- >>> let fileOrigin4 = FileSpecifiedByUser "thisdirdoesnotexist" -- >>> toListM $ fileListProducer Recursive fileOrigin4 -- [FileReaderErr (FileSpecifiedByUser "thisdirdoesnotexist") thisdirdoesnotexist: openBinaryFile: does not exist (No such file or directory) (Just ...)] fileListProducer :: forall m. MonadIO m => Recursive -> FileOrigin -> Producer' (FileReader Handle) m () fileListProducer recursive = go where go :: FileOrigin -> Producer' (FileReader Handle) m () go fileOrigin = do let maybeFilePath = getFilePathFromFileOrigin fileOrigin case maybeFilePath of -- This is standard input. We don't currently handle this, so just -- return unit. Nothing -> return () -- This is a normal file. Not standard input. Just filePath -> do eitherHandle <- openFilePathForReading filePath case eitherHandle of Right handle -> yield $ FileReaderSuccess fileOrigin handle Left fileIOErr -> if recursive == Recursive then do let fileListM = toListM $ childOf filePath eitherFileList <- liftIO $ try fileListM case eitherFileList of Left dirIOErr -> yield $ FileReaderErr fileOrigin fileIOErr (Just dirIOErr) Right fileList -> do let sortedFileList = sort fileList let fileOrigins = fmap FileFoundRecursively sortedFileList let lalas = fmap (fileListProducer recursive) fileOrigins for (each lalas) id else yield $ FileReaderErr fileOrigin fileIOErr Nothing ----------------------- -- Filename Handling -- ----------------------- -- | This data type specifies how printing filenames will be handled, along -- with the 'computeFilenameHandlingFromFiles' function. data FilenameHandlingFromFiles = NoFilename -- ^ Do not print the filename on stdout. | PrintFilename -- ^ Print the filename on stdout. deriving (Eq, Read, Show) -- | Given a 'Producer' of 'FileReader's, figure out whether or not we should -- print the filename of the file to stdout. -- -- The following examples walk through possible command lines using @highlight@, and the corresponding return values of this function. -- -- @ -- $ highlight expression -- @ -- -- We want to read from stdin. There should be no 'FileReaders' in the -- 'Producer'. Do not print the filename. -- -- >>> let producerStdin = each [] -- >>> (fhStdin, _) <- computeFilenameHandlingFromFiles producerStdin -- >>> fhStdin -- NoFilename -- -- @ -- $ highlight expression file1 -- @ -- -- We want to highlight a single file. There should only be a single -- 'FileReader' in the 'Producer', and it should be 'FileSpecifiedByUser'. Do -- not print the filename. -- -- >>> let fileOriginSingleFile = FileSpecifiedByUser "file1" -- >>> let fileReaderSingleFile = FileReaderSuccess fileOriginSingleFile "hello" -- >>> let producerSingleFile = each [fileReaderSingleFile] -- >>> (fhSingleFile, _) <- computeFilenameHandlingFromFiles producerSingleFile -- >>> fhSingleFile -- NoFilename -- -- @ -- $ highlight expression file1 file2 -- @ -- -- We want to highlight two files. Print the filename. -- -- >>> let fileOriginMulti1 = FileSpecifiedByUser "file1" -- >>> let fileReaderMulti1 = FileReaderSuccess fileOriginMulti1 "hello" -- >>> let fileOriginMulti2 = FileSpecifiedByUser "file2" -- >>> let fileReaderMulti2 = FileReaderSuccess fileOriginMulti2 "bye" -- >>> let producerMultiFile = each [fileReaderMulti1, fileReaderMulti2] -- >>> (fhMultiFile, _) <- computeFilenameHandlingFromFiles producerMultiFile -- >>> fhMultiFile -- PrintFilename -- -- @ -- $ highlight -r expression dir1 -- @ -- -- We want to highlight all files found in @dir1\/@. Print filenames. -- -- >>> let fileOriginRec = FileFoundRecursively "dir1/file1" -- >>> let fileReaderRec = FileReaderSuccess fileOriginRec "cat" -- >>> let producerRec = each [fileReaderRec] -- >>> (fhRec, _) <- computeFilenameHandlingFromFiles producerRec -- >>> fhRec -- PrintFilename computeFilenameHandlingFromFiles :: forall a m r. Monad m => Producer (FileReader a) m r -> m (FilenameHandlingFromFiles, Producer (FileReader a) m r) computeFilenameHandlingFromFiles producer1 = do eitherFileReader1 <- next producer1 case eitherFileReader1 of Left ret -> return (NoFilename, return ret) Right (fileReader1, producer2) -> do let fileOrigin1 = getFileOriginFromFileReader fileReader1 case fileOrigin1 of Stdin -> error "Not currenty handling stdin..." FileSpecifiedByUser _ -> do eitherSecondFile <- next producer2 case eitherSecondFile of Left ret2 -> return (NoFilename, yield fileReader1 *> return ret2) Right (fileReader2, producer3) -> return ( PrintFilename , yield fileReader1 *> yield fileReader2 *> producer3 ) FileFoundRecursively _ -> return (PrintFilename, yield fileReader1 *> producer2)