highlight (empty) → 0.1.0.0
raw patch · 46 files changed
+2679/−0 lines, 46 filesdep +QuickCheckdep +Win32dep +ansi-terminalsetup-changedbinary-added
Dependencies added: QuickCheck, Win32, ansi-terminal, base, base-compat, bytestring, containers, criterion, directory, doctest, filepath, highlight, lens, mtl, mtl-compat, optparse-applicative, pipes, pipes-bytestring, pipes-group, pipes-safe, process, regex, regex-with-pcre, semigroups, system-filepath, tasty, tasty-golden, text, transformers, transformers-compat, unix
Files
- LICENSE +30/−0
- README.md +52/−0
- Setup.hs +2/−0
- app/highlight/Main.hs +7/−0
- app/hrep/Main.hs +7/−0
- bench/Bench.hs +14/−0
- highlight.cabal +147/−0
- img/highlight-example-screenshot.png binary
- src/Highlight/Common/Color.hs +187/−0
- src/Highlight/Common/Error.hs +21/−0
- src/Highlight/Common/Monad.hs +134/−0
- src/Highlight/Common/Monad/Input.hs +401/−0
- src/Highlight/Common/Monad/Output.hs +263/−0
- src/Highlight/Common/Options.hs +172/−0
- src/Highlight/Highlight.hs +35/−0
- src/Highlight/Highlight/Monad.hs +81/−0
- src/Highlight/Highlight/Options.hs +92/−0
- src/Highlight/Highlight/Run.hs +125/−0
- src/Highlight/Hrep.hs +23/−0
- src/Highlight/Hrep/Monad.hs +32/−0
- src/Highlight/Hrep/Run.hs +112/−0
- src/Highlight/Pipes.hs +154/−0
- src/Highlight/Util.hs +103/−0
- test/DocTest.hs +36/−0
- test/Test.hs +19/−0
- test/Test/Golden.hs +310/−0
- test/golden/golden-files/highlight/from-grep.stderr +0/−0
- test/golden/golden-files/highlight/from-grep.stdout +14/−0
- test/golden/golden-files/highlight/multi-file.stderr +1/−0
- test/golden/golden-files/highlight/multi-file.stdout +29/−0
- test/golden/golden-files/highlight/single-file.stderr +0/−0
- test/golden/golden-files/highlight/single-file.stdout +5/−0
- test/golden/golden-files/hrep/from-stdin.stderr +0/−0
- test/golden/golden-files/hrep/from-stdin.stdout +3/−0
- test/golden/golden-files/hrep/multi-file.stderr +1/−0
- test/golden/golden-files/hrep/multi-file.stdout +9/−0
- test/golden/golden-files/hrep/single-file.stderr +0/−0
- test/golden/golden-files/hrep/single-file.stdout +3/−0
- test/golden/test-files/dir1/file3 +9/−0
- test/golden/test-files/dir1/file4 +9/−0
- test/golden/test-files/dir1/subdir1/file5 +6/−0
- test/golden/test-files/dir2/file6 +5/−0
- test/golden/test-files/empty-file +0/−0
- test/golden/test-files/file1 +5/−0
- test/golden/test-files/file2 +7/−0
- test/golden/test-files/from-grep +14/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Dennis Gosnell (c) 2017++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Author name here nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,52 @@++Highlight+=========++[](http://travis-ci.org/cdepillabout/highlight)+[](https://hackage.haskell.org/package/highlight)+[](http://stackage.org/lts/package/highlight)+[](http://stackage.org/nightly/package/highlight)+++`highlight` is a ++For example, imagine the following Haskell data types and values:++```haskell+data Foo = Foo { foo1 :: Integer , foo2 :: [String] } deriving Show++foo :: Foo+foo = Foo 3 ["hello", "goodbye"]++data Bar = Bar { bar1 :: Double , bar2 :: [Foo] } deriving Show++bar :: Bar+bar = Bar 10.55 [foo, foo]+```++If you run this in `ghci` and type `print bar`, you'll get output like this:++```haskell+> print bar+Bar {bar1 = 10.55, bar2 = [Foo {foo1 = 3, foo2 = ["hello","goodbye"]},Foo {foo1 = 3, foo2 = ["hello","goodbye"]}]}+```++This is pretty hard to read. Imagine if there were more fields or it were even+more deeply nested. It would be even more difficult to read.++`pretty-simple` can be used to print `bar` in an easy-to-read format:++++## Usage++## Features++## Why not `(some other package)`?++## Contributions++Feel free to open an+[issue](https://github.com/cdepillabout/pretty-simple/issues) or+[PR](https://github.com/cdepillabout/pretty-simple/pulls) for any+bugs/problems/suggestions/improvements.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/highlight/Main.hs view
@@ -0,0 +1,7 @@++module Main where++import Highlight.Highlight (defaultMain)++main :: IO ()+main = defaultMain
+ app/hrep/Main.hs view
@@ -0,0 +1,7 @@++module Main where++import Highlight.Hrep (defaultMain)++main :: IO ()+main = defaultMain
+ bench/Bench.hs view
@@ -0,0 +1,14 @@++module Main where++import Criterion.Main (bench, bgroup, defaultMain, nf)++main :: IO ()+main =+ defaultMain+ [ bgroup+ "example1"+ [ bench "plus" $ nf (1 +) (1 :: Int)+ , bench "minus" $ nf (1 -) (1 :: Int)+ ]+ ]
+ highlight.cabal view
@@ -0,0 +1,147 @@+name: highlight+version: 0.1.0.0+synopsis: Command line tool for highlighting parts of files matching a regex.+description: Please see <https://github.com/cdepillabout/highlight#readme README.md>.+homepage: https://github.com/cdepillabout/highlight+license: BSD3+license-file: LICENSE+author: Dennis Gosnell+maintainer: cdep.illabout@gmail.com+copyright: 2017 Dennis Gosnell+category: Text+build-type: Simple+extra-source-files: README.md+ , img/highlight-example-screenshot.png+ , test/golden/golden-files/highlight/from-grep.stderr+ , test/golden/golden-files/highlight/from-grep.stdout+ , test/golden/golden-files/highlight/multi-file.stderr+ , test/golden/golden-files/highlight/multi-file.stdout+ , test/golden/golden-files/highlight/single-file.stderr+ , test/golden/golden-files/highlight/single-file.stdout+ , test/golden/golden-files/hrep/from-stdin.stderr+ , test/golden/golden-files/hrep/from-stdin.stdout+ , test/golden/golden-files/hrep/multi-file.stderr+ , test/golden/golden-files/hrep/multi-file.stdout+ , test/golden/golden-files/hrep/single-file.stderr+ , test/golden/golden-files/hrep/single-file.stdout+ , test/golden/test-files/dir1/file3+ , test/golden/test-files/dir1/file4+ , test/golden/test-files/dir1/subdir1/file5+ , test/golden/test-files/dir2/file6+ , test/golden/test-files/empty-file+ , test/golden/test-files/file1+ , test/golden/test-files/file2+ , test/golden/test-files/from-grep+cabal-version: >=1.10++library+ hs-source-dirs: src+ exposed-modules: Highlight.Common.Color+ , Highlight.Common.Error+ , Highlight.Common.Monad+ , Highlight.Common.Monad.Input+ , Highlight.Common.Monad.Output+ , Highlight.Common.Options+ , Highlight.Highlight+ , Highlight.Highlight.Monad+ , Highlight.Highlight.Options+ , Highlight.Highlight.Run+ , Highlight.Hrep+ , Highlight.Hrep.Monad+ , Highlight.Hrep.Run+ , Highlight.Pipes+ , Highlight.Util+ build-depends: base >= 4.7 && < 5+ , base-compat >= 0.8+ , ansi-terminal >= 0.6+ , bytestring >= 0.9+ , containers >= 0.5+ , directory >= 1.2+ , filepath >= 1+ , lens >= 3+ , mtl >= 2.0+ , mtl-compat >= 0.2+ , optparse-applicative >= 0.11+ , pipes >= 4+ , pipes-bytestring >= 2+ , pipes-group >= 1+ , pipes-safe >= 2+ , regex >= 0.10+ , regex-with-pcre >= 1.0+ , semigroups >= 0.15+ , system-filepath >= 0.4+ , text >= 1.2+ , transformers >= 0.2+ , transformers-compat >= 0.3+ if os(windows)+ Build-Depends: Win32 >= 2.0+ else+ Build-Depends: unix >= 2.0++ default-language: Haskell2010+ ghc-options: -Wall+ other-extensions: TemplateHaskell++executable highlight+ main-is: Main.hs+ hs-source-dirs: app/highlight+ build-depends: base+ , highlight+ default-language: Haskell2010+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N++executable hrep+ main-is: Main.hs+ hs-source-dirs: app/hrep+ build-depends: base+ , highlight+ default-language: Haskell2010+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N++test-suite highlight-doctest+ type: exitcode-stdio-1.0+ main-is: DocTest.hs+ hs-source-dirs: test+ build-depends: base+ , doctest+ , QuickCheck+ default-language: Haskell2010+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N++test-suite highlight-test+ type: exitcode-stdio-1.0+ main-is: Test.hs+ other-modules: Test.Golden+ hs-source-dirs: test+ build-depends: base+ , base-compat+ , bytestring+ , directory+ , highlight+ , lens+ , pipes+ , process+ , tasty+ , tasty-golden+ , transformers+ , transformers-compat+ if os(windows)+ Build-Depends: Win32+ else+ Build-Depends: unix+ default-language: Haskell2010+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N++benchmark highlight-bench+ type: exitcode-stdio-1.0+ main-is: Bench.hs+ hs-source-dirs: bench+ build-depends: base+ , criterion+ , highlight+ default-language: Haskell2010+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N++source-repository head+ type: git+ location: git@github.com:cdepillabout/highlight.git
+ img/highlight-example-screenshot.png view
binary file changed (absent → 15319 bytes)
+ src/Highlight/Common/Color.hs view
@@ -0,0 +1,187 @@+{-# LANGUAGE OverloadedStrings #-}++module Highlight.Common.Color where++import Prelude ()+import Prelude.Compat++import Data.ByteString.Char8 (ByteString, empty, pack)+import Data.IntMap.Strict (IntMap, (!), fromList)+import System.Console.ANSI+ (Color(..), ColorIntensity(..), ConsoleIntensity(..),+ ConsoleLayer(..), SGR(..), setSGRCode)++------------------------+-- Application Colors --+------------------------++-- | Find the corresponding color for the number in 'allColorsList', taking the+-- mod of the 'Int'.+--+-- >>> colorForFileNumber 0+-- "\ESC[1m\ESC[94m"+-- >>> colorForFileNumber 1+-- "\ESC[1m\ESC[92m"+-- >>> colorForFileNumber 4+-- "\ESC[1m\ESC[94m"+colorForFileNumber :: Int -> ByteString+colorForFileNumber num = allColorsMap ! (num `mod` allColorsLength)++-- | 'allColorsList' turned into an 'IntMap' for faster lookup.+allColorsMap :: IntMap ByteString+allColorsMap = fromList $ zip [0..] allColorsList++-- | 'length' of 'allColorsList'.+allColorsLength :: Int+allColorsLength = length allColorsList++-- | List of all the colors that are used for highlighting filenames.+allColorsList :: [ByteString]+allColorsList =+ [ colorVividBlueBold+ , colorVividGreenBold+ , colorVividCyanBold+ , colorVividMagentaBold+ ]++-----------------------+-- Vivid Bold Colors --+-----------------------++colorVividBlackBold :: ByteString+colorVividBlackBold = colorBold `mappend` colorVividBlack++colorVividBlueBold :: ByteString+colorVividBlueBold = colorBold `mappend` colorVividBlue++colorVividCyanBold :: ByteString+colorVividCyanBold = colorBold `mappend` colorVividCyan++colorVividGreenBold :: ByteString+colorVividGreenBold = colorBold `mappend` colorVividGreen++colorVividMagentaBold :: ByteString+colorVividMagentaBold = colorBold `mappend` colorVividMagenta++colorVividRedBold :: ByteString+colorVividRedBold = colorBold `mappend` colorVividRed++colorVividWhiteBold :: ByteString+colorVividWhiteBold = colorBold `mappend` colorVividWhite++colorVividYellowBold :: ByteString+colorVividYellowBold = colorBold `mappend` colorVividYellow++-----------------------+-- Dull Bold Colors --+-----------------------++colorDullBlackBold :: ByteString+colorDullBlackBold = colorBold `mappend` colorDullBlack++colorDullBlueBold :: ByteString+colorDullBlueBold = colorBold `mappend` colorDullBlue++colorDullCyanBold :: ByteString+colorDullCyanBold = colorBold `mappend` colorDullCyan++colorDullGreenBold :: ByteString+colorDullGreenBold = colorBold `mappend` colorDullGreen++colorDullMagentaBold :: ByteString+colorDullMagentaBold = colorBold `mappend` colorDullMagenta++colorDullRedBold :: ByteString+colorDullRedBold = colorBold `mappend` colorDullRed++colorDullWhiteBold :: ByteString+colorDullWhiteBold = colorBold `mappend` colorDullWhite++colorDullYellowBold :: ByteString+colorDullYellowBold = colorBold `mappend` colorDullYellow++------------------+-- Vivid Colors --+------------------++colorVividBlack :: ByteString+colorVividBlack = colorHelper Vivid Black++colorVividBlue :: ByteString+colorVividBlue = colorHelper Vivid Blue++colorVividCyan :: ByteString+colorVividCyan = colorHelper Vivid Cyan++colorVividGreen :: ByteString+colorVividGreen = colorHelper Vivid Green++colorVividMagenta :: ByteString+colorVividMagenta = colorHelper Vivid Magenta++colorVividRed :: ByteString+colorVividRed = colorHelper Vivid Red++colorVividWhite :: ByteString+colorVividWhite = colorHelper Vivid White++colorVividYellow :: ByteString+colorVividYellow = colorHelper Vivid Yellow++------------------+-- Dull Colors --+------------------++colorDullBlack :: ByteString+colorDullBlack = colorHelper Dull Black++colorDullBlue :: ByteString+colorDullBlue = colorHelper Dull Blue++colorDullCyan :: ByteString+colorDullCyan = colorHelper Dull Cyan++colorDullGreen :: ByteString+colorDullGreen = colorHelper Dull Green++colorDullMagenta :: ByteString+colorDullMagenta = colorHelper Dull Magenta++colorDullRed :: ByteString+colorDullRed = colorHelper Dull Red++colorDullWhite :: ByteString+colorDullWhite = colorHelper Dull White++colorDullYellow :: ByteString+colorDullYellow = colorHelper Dull Yellow++--------------------+-- Special Colors --+--------------------++-- | Change the intensity to 'BoldIntensity'.+colorBold :: ByteString+colorBold = setSGRCodeBuilder [SetConsoleIntensity BoldIntensity]++-- | 'Reset' the console color back to normal.+colorReset :: ByteString+colorReset = setSGRCodeBuilder [Reset]++-- | Empty string.+colorNull :: ByteString+colorNull = empty++-------------+-- Helpers --+-------------++-- | Helper for creating a 'ByteString' for an ANSI escape sequence color based on+-- a 'ColorIntensity' and a 'Color'.+colorHelper :: ColorIntensity -> Color -> ByteString+colorHelper colorIntensity color =+ setSGRCodeBuilder [SetColor Foreground colorIntensity color]++-- | Convert a list of 'SGR' to a 'ByteString'.+setSGRCodeBuilder :: [SGR] -> ByteString+setSGRCodeBuilder = pack . setSGRCode
+ src/Highlight/Common/Error.hs view
@@ -0,0 +1,21 @@++module Highlight.Common.Error where++import Prelude ()+import Prelude.Compat++import Data.Monoid ((<>))++import Highlight.Common.Options (RawRegex(RawRegex))+import Highlight.Util (die)++-- | Sum-type representing all errors that can be thrown by this application.+data HighlightErr+ = HighlightRegexCompileErr RawRegex+ -- ^ Error when trying to compile the 'RawRegex' into a regular expression.+ deriving Show++-- | Call 'die' with an error message based on 'HighlightErr'.+handleErr :: HighlightErr -> IO a+handleErr (HighlightRegexCompileErr (RawRegex regex)) =+ die 10 $ "Regex not well formed: " <> regex
+ src/Highlight/Common/Monad.hs view
@@ -0,0 +1,134 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE RankNTypes #-}++module Highlight.Common.Monad+ ( module Highlight.Common.Monad+ , module Highlight.Common.Monad.Input+ , module Highlight.Common.Monad.Output+ ) where++import Prelude ()+import Prelude.Compat++import Control.Lens (view)+import Control.Monad.Except (ExceptT, MonadError, runExceptT, throwError)+import Control.Monad.IO.Class (MonadIO)+import Control.Monad.Reader (MonadReader, ReaderT, runReaderT)+import Control.Monad.State (MonadState, StateT, evalStateT)+import Text.RE.PCRE+ (RE, SimpleREOptions(MultilineInsensitive, MultilineSensitive),+ compileRegexWith)++import Highlight.Common.Error (HighlightErr(..))+import Highlight.Common.Monad.Input+ (FilenameHandlingFromFiles(NoFilename, PrintFilename), InputData,+ createInputData)+import Highlight.Common.Monad.Output+ (Output(OutputStderr, OutputStdout), handleInputData,+ runOutputProducer)+import Highlight.Common.Options+ (HasIgnoreCase(ignoreCaseLens),+ HasInputFilenames(inputFilenamesLens), HasRecursive(recursiveLens),+ HasRawRegex(rawRegexLens), IgnoreCase(DoNotIgnoreCase, IgnoreCase),+ InputFilename, RawRegex(RawRegex), Recursive)++--------------------------------+-- The Common Highlight Monad --+--------------------------------++-- | This is the common monad for both @highlight@ and @hrep@. It has been+-- kept polymorphic here so it can be easily specialized by @highlight@ and+-- @hrep@.+--+-- @r@ is the options or config type. @s@ is the state. @e@ is the error.+newtype CommonHighlightM r s e a = CommonHighlightM+ { unCommonHighlightM :: ReaderT r (StateT s (ExceptT e IO)) a+ } deriving ( Functor+ , Applicative+ , Monad+ , MonadError e+ , MonadIO+ , MonadReader r+ , MonadState s+ )++-- | Given an @r@ and @s@, run 'CommonHighlightM'.+runCommonHighlightM :: r -> s -> CommonHighlightM r s e a -> IO (Either e a)+runCommonHighlightM r s =+ runExceptT .+ flip evalStateT s .+ flip runReaderT r .+ unCommonHighlightM++-- | Get the 'IgnoreCase' option.+getIgnoreCaseM :: (HasIgnoreCase r, MonadReader r m) => m IgnoreCase+getIgnoreCaseM = view ignoreCaseLens++-- | Get the 'Recursive' option.+getRecursiveM :: (HasRecursive r, MonadReader r m) => m Recursive+getRecursiveM = view recursiveLens++-- | Get the 'RawRegex' option.+getRawRegexM :: (HasRawRegex r, MonadReader r m) => m RawRegex+getRawRegexM = view rawRegexLens++-- | Get a list of the 'InputFilename'.+getInputFilenamesM+ :: (HasInputFilenames r, MonadReader r m) => m [InputFilename]+getInputFilenamesM = view inputFilenamesLens++------------------+-- Throw Errors --+------------------++-- | Throw a 'HighlightErr'.+throwHighlightErr :: HighlightErr -> CommonHighlightM r s HighlightErr a+throwHighlightErr = throwError++-- | Throw a 'HighlightRegexCompileErr'.+throwRegexCompileErr :: RawRegex -> CommonHighlightM r s HighlightErr a+throwRegexCompileErr = throwHighlightErr . HighlightRegexCompileErr++-----------+-- Regex --+-----------++-- | Call 'compileHighlightRegex'. Throw a 'HighlightErr' if the regex cannot+-- be compiled.+compileHighlightRegexWithErr+ :: (HasIgnoreCase r, HasRawRegex r)+ => CommonHighlightM r s HighlightErr RE+compileHighlightRegexWithErr = do+ ignoreCase <- getIgnoreCaseM+ rawRegex <- getRawRegexM+ case compileHighlightRegex ignoreCase rawRegex of+ Just re -> return re+ Nothing -> throwRegexCompileErr rawRegex++-- | Try compiling a 'RawRegex' into a 'RE'.+--+-- Setup for examples:+--+-- >>> import Data.Maybe (isJust)+--+-- Return 'Just' for a proper regex:+--+-- >>> isJust $ compileHighlightRegex IgnoreCase (RawRegex "good regex")+-- True+--+-- Return 'Nothing' for an improper regex:+--+-- >>> isJust $ compileHighlightRegex IgnoreCase (RawRegex "bad regex (")+-- False+compileHighlightRegex :: IgnoreCase -> RawRegex -> Maybe RE+compileHighlightRegex ignoreCase (RawRegex rawRegex) =+ let simpleREOptions =+ case ignoreCase of+ IgnoreCase -> MultilineInsensitive+ DoNotIgnoreCase -> MultilineSensitive+ in compileRegexWith simpleREOptions rawRegex
+ src/Highlight/Common/Monad/Input.hs view
@@ -0,0 +1,401 @@+{-# 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)
+ src/Highlight/Common/Monad/Output.hs view
@@ -0,0 +1,263 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE Rank2Types #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Highlight.Common.Monad.Output+ ( Output(..)+ , handleInputData+ , runOutputProducer+ ) where++import Prelude ()+import Prelude.Compat++import Control.Exception (IOException)+import Control.Monad.IO.Class (MonadIO)+import Control.Monad.State (MonadState, StateT, evalStateT, get)+import Control.Monad.Trans.Class (lift)+import Data.ByteString (ByteString)+import Pipes+ (Consumer, Pipe, Producer, Producer', Proxy, (>->), await, each,+ runEffect, yield)+import Pipes.ByteString (stdout)++import Highlight.Common.Monad.Input+ (FilenameHandlingFromFiles, FileOrigin,+ FileReader(FileReaderErr, FileReaderSuccess),+ InputData(InputData), getFileOriginFromFileReader,+ getFilePathFromFileReader)+import Highlight.Pipes (stderrConsumer)+import Highlight.Util+ (convertStringToRawByteString, modify', whenNonNull)++-----------------------------+-- Convert input to Output --+-----------------------------++-- | The current number of the file we are outputting. This is increased by+-- one for each file.+type ColorNum = Int++-- | The state for which number file we are outputting with its 'FileOrigin'.+data FileColorState+ = FileColorState !(Maybe FileOrigin) {-# UNPACK #-} !ColorNum+ deriving (Eq, Read, Show)++-- | Initial value for 'FileColorState'. Defined as the following:+--+-- >>> defFileColorState+-- FileColorState Nothing 0+defFileColorState :: FileColorState+defFileColorState = FileColorState Nothing 0++-- | Convert 'InputData' to 'Output'.+handleInputData+ :: forall m.+ MonadIO m+ => (ByteString -> m [ByteString])+ -- ^ Function to use for conversion for a line from stdin.+ -> (FilenameHandlingFromFiles+ -> ByteString+ -> Int+ -> ByteString+ -> m [ByteString]+ )+ -- ^ Function to use for conversion for a line from a normal file.+ -> (ByteString -> IOException -> Maybe IOException -> m [ByteString])+ -- ^ Function to use for conversion for an io error.+ -> InputData m ()+ -- ^ All of the input lines.+ -> Producer Output m ()+handleInputData stdinF nonErrF errF (InputData nameHandling producer) =+ producer >-> evalStateT go defFileColorState+ where+ go :: StateT FileColorState (Pipe (FileReader ByteString) Output m) ()+ go = do+ fileReader <- lift await+ let maybeFilePath = getFilePathFromFileReader fileReader+ fileOrigin = getFileOriginFromFileReader fileReader+ colorNumIfNewFile <- getColorNumIfNewFileM fileOrigin+ case maybeFilePath of+ -- The current @'FileReader' 'ByteString'@ is from stdin.+ Nothing ->+ case fileReader of+ FileReaderSuccess _ line -> do+ outByteStrings <- lift . lift $ stdinF line+ toStdoutWhenNonNull outByteStrings fileOrigin+ FileReaderErr _ _ _ -> return ()+ -- The current @'FileReader' 'ByteString'@ is from a normal file.+ Just filePath -> do+ byteStringFilePath <- convertStringToRawByteString filePath+ case fileReader of+ FileReaderErr _ ioerr maybeioerr -> do+ outByteStrings <-+ lift . lift $ errF byteStringFilePath ioerr maybeioerr+ toStderrWhenNonNull outByteStrings+ FileReaderSuccess _ inputLine -> do+ outByteStrings <-+ lift . lift $+ nonErrF+ nameHandling+ byteStringFilePath+ colorNumIfNewFile+ inputLine+ toStdoutWhenNonNull outByteStrings fileOrigin+ go++-- | When the list of 'ByteString' is not @[]@, convert them to 'OutputStdout'+-- and return them in a 'Producer'. Call 'updateColorNum' with the passed-in+-- 'FileOrigin'.+toStdoutWhenNonNull+ :: forall m x' x.+ Monad m+ => [ByteString]+ -> FileOrigin+ -> StateT FileColorState (Proxy x' x () Output m) ()+toStdoutWhenNonNull outByteStrings fileOrigin =+ whenNonNull outByteStrings $ do+ lift $ toStdoutWithNewline outByteStrings+ updateColorNumM fileOrigin++-- | When the list of 'ByteString' is not @[]@, convert them to 'OutputStderr'+-- and return them in a 'Producer'. Do not call 'updateColorNum'.+toStderrWhenNonNull+ :: forall m s x' x.+ Monad m+ => [ByteString] -> StateT s (Proxy x' x () Output m) ()+toStderrWhenNonNull outByteStrings =+ whenNonNull outByteStrings . lift $ toStderrWithNewline outByteStrings++-- | Call 'updateColorNum' with the current value of 'FileColorState'.+updateColorNumM+ :: MonadState FileColorState m => FileOrigin -> m ()+updateColorNumM = modify' . updateColorNum++-- | Based on the value of the passed in 'FileOrigin' and the 'FileOrigin' in+-- 'FileColorState', update the 'ColorNum' in 'FileColorState'.+--+-- Setup for examples:+--+-- >>> import Highlight.Common.Monad.Input (FileOrigin(FileSpecifiedByUser))+--+-- If 'defFileColorState' is passed in, then update the 'FileOrigin'.+--+-- >>> let fileOrigin = FileSpecifiedByUser "hello.txt"+-- >>> updateColorNum fileOrigin defFileColorState+-- FileColorState (Just (FileSpecifiedByUser "hello.txt")) 0+--+-- If the 'FileOrigin' is different from the one in 'FileColorState', then+-- increment the 'ColorNum'.+--+-- >>> let oldFileOrigin = FileSpecifiedByUser "hello.txt"+-- >>> let newFileOrigin = FileSpecifiedByUser "bye.txt"+-- >>> let fileColorStateDiff = FileColorState (Just oldFileOrigin) 5+-- >>> updateColorNum newFileOrigin fileColorStateDiff+-- FileColorState (Just (FileSpecifiedByUser "bye.txt")) 6+--+-- If the 'FileOrigin' is the same as the one in 'FileColorState', then do not+-- increment the 'ColorNum'.+--+-- >>> let sameFileOrigin = FileSpecifiedByUser "hello.txt"+-- >>> let fileColorStateDiff = FileColorState (Just sameFileOrigin) 5+-- >>> updateColorNum sameFileOrigin fileColorStateDiff+-- FileColorState (Just (FileSpecifiedByUser "hello.txt")) 5+updateColorNum+ :: FileOrigin+ -> FileColorState+ -> FileColorState+updateColorNum newFileOrigin (FileColorState Nothing colorNum) =+ FileColorState (Just newFileOrigin) colorNum+updateColorNum newFileOrigin (FileColorState (Just prevFileOrigin) colorNum)+ | prevFileOrigin == newFileOrigin =+ FileColorState (Just newFileOrigin) colorNum+ | otherwise = FileColorState (Just newFileOrigin) (colorNum + 1)++-- | This called 'updateColorNum' and returns the new value of 'ColorNum', but+-- it doesn't update the 'FileColorNum' in 'MonadState'.+getColorNumIfNewFileM+ :: MonadState FileColorState m+ => FileOrigin -> m ColorNum+getColorNumIfNewFileM newFileOrigin = do+ fileColorState <- get+ let (FileColorState _ colorNum) = updateColorNum newFileOrigin fileColorState+ return colorNum++-- | This is just 'toOutputWithNewLine' 'OutputStderr'.+toStderrWithNewline :: Monad m => [ByteString] -> Producer' Output m ()+toStderrWithNewline = toOutputWithNewline OutputStderr++-- | This is just 'toOutputWithNewLine' 'OutputStdout'.+toStdoutWithNewline :: Monad m => [ByteString] -> Producer' Output m ()+toStdoutWithNewline = toOutputWithNewline OutputStdout++-- | Convert a list of 'ByteString' to 'Output' with the given function.+--+-- Setup for examples:+--+-- >>> :set -XOverloadedStrings+-- >>> import Pipes.Prelude (toListM)+--+-- If the list of 'ByteString' is empty, then do nothing.+--+-- >>> toListM $ toOutputWithNewline OutputStdout []+-- []+--+-- If the list of 'ByteString' is not empty, then convert to 'Output' and add+-- an ending newline.+--+-- >>> toListM $ toOutputWithNewline OutputStderr ["hello", "bye"]+-- [OutputStderr "hello",OutputStderr "bye",OutputStderr "\n"]+toOutputWithNewline+ :: Monad m+ => (ByteString -> Output)+ -- ^ Function to use to convert the 'ByteString' to 'Output'.+ -> [ByteString]+ -- ^ List of 'ByteString's to convert to 'Output'.+ -> Producer' Output m ()+toOutputWithNewline _ [] = return ()+toOutputWithNewline byteStringToOutput byteStrings = do+ each $ fmap byteStringToOutput byteStrings+ yield $ byteStringToOutput "\n"++------------+-- Output --+------------++-- | Sum-type to represent where a given 'ByteString' should be output, whether+-- it is stdout or stderr.+data Output+ = OutputStdout !ByteString+ | OutputStderr !ByteString+ deriving (Eq, Read, Show)++-- | Write each 'Output' to either 'stdout' or 'stderrConsumer'.+--+-- Setup for tests:+--+-- >>> import Pipes (runEffect)+--+-- Send 'OutputStdout' to 'stdout'.+--+-- >>> runEffect $ yield (OutputStdout "this goes to stdout") >-> outputConsumer+-- this goes to stdout+--+-- Send 'OutputStderr' to 'stderrConsumer'.+--+-- >>> runEffect $ yield (OutputStderr "this goes to stderr") >-> outputConsumer+-- this goes to stderr+outputConsumer :: MonadIO m => Consumer Output m ()+outputConsumer = do+ output <- await+ case output of+ OutputStdout byteString ->+ yield byteString >-> stdout+ OutputStderr byteString ->+ yield byteString >-> stderrConsumer+ outputConsumer++-- | Run a 'Producer' 'Output' by connecting it to 'outputConsumer'.+runOutputProducer :: forall m. MonadIO m => Producer Output m () -> m ()+runOutputProducer producer = runEffect $ producer >-> outputConsumer
+ src/Highlight/Common/Options.hs view
@@ -0,0 +1,172 @@+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE OverloadedStrings #-}++module Highlight.Common.Options where++import Prelude ()+import Prelude.Compat++import Control.Applicative (many)+import Control.Lens (Lens', lens)+import Data.Monoid ((<>))+import Data.String (IsString)+import Options.Applicative+ (Parser, flag, help, long, metavar, short, strArgument)++-----------------+-- Ignore case --+-----------------++-- | Whether or not the case of a regular expression should be ignored.+-- Similar to @grep@'s @--ignore-case@ option.+data IgnoreCase = IgnoreCase | DoNotIgnoreCase+ deriving (Eq, Read, Show)++class HasIgnoreCase r where+ ignoreCaseLens :: Lens' r IgnoreCase+ default ignoreCaseLens :: HasCommonOptions r => Lens' r IgnoreCase+ ignoreCaseLens = commonOptionsLens . ignoreCaseLens++ignoreCaseParser :: Parser IgnoreCase+ignoreCaseParser =+ flag+ DoNotIgnoreCase+ IgnoreCase+ (long "ignore-case" <> short 'i' <> help "ignore case distinctions")++---------------+-- Recursive --+---------------++-- | Whether or not files should be searched recursively. Similar to @grep@'s+-- @--recursive@ option.+data Recursive = Recursive | NotRecursive+ deriving (Eq, Read, Show)++class HasRecursive r where+ recursiveLens :: Lens' r Recursive+ default recursiveLens :: HasCommonOptions r => Lens' r Recursive+ recursiveLens = commonOptionsLens . recursiveLens++recursiveParser :: Parser Recursive+recursiveParser =+ let mods =+ long "recursive" <>+ short 'r' <>+ help "recursive operate on files under specified directory"+ in flag NotRecursive Recursive mods++---------------+-- Raw regex --+---------------++-- | The raw, pre-compiled regular expression passed in on the command line by+-- the user.+newtype RawRegex = RawRegex+ { unRawRegex :: String+ } deriving (Eq, IsString, Read, Show)++class HasRawRegex r where+ rawRegexLens :: Lens' r RawRegex+ default rawRegexLens :: HasCommonOptions r => Lens' r RawRegex+ rawRegexLens = commonOptionsLens . rawRegexLens++rawRegexParser :: Parser RawRegex+rawRegexParser =+ let mods = metavar "PATTERN"+ in RawRegex <$> strArgument mods++--------------------+-- input filename --+--------------------++-- | An input file passed in on the command line by the user.+newtype InputFilename = InputFilename+ { unInputFilename :: FilePath+ } deriving (Eq, IsString, Read, Show)++class HasInputFilenames r where+ inputFilenamesLens :: Lens' r [InputFilename]+ default inputFilenamesLens :: HasCommonOptions r => Lens' r [InputFilename]+ inputFilenamesLens = commonOptionsLens . inputFilenamesLens++inputFilenamesParser :: Parser [InputFilename]+inputFilenamesParser =+ let mods = metavar "FILE"+ in many $ InputFilename <$> strArgument mods++--------------------+-- common options --+--------------------++-- | A set of options that are common to both the @highlight@ and @hrep@+-- applications.+data CommonOptions = CommonOptions+ { commonOptionsIgnoreCase :: IgnoreCase+ , commonOptionsRecursive :: Recursive+ , commonOptionsRawRegex :: RawRegex+ , commonOptionsInputFilenames :: [InputFilename]+ } deriving (Eq, Read, Show)++class HasCommonOptions r where+ commonOptionsLens :: Lens' r CommonOptions++instance HasCommonOptions CommonOptions where+ commonOptionsLens :: Lens' CommonOptions CommonOptions+ commonOptionsLens = id++instance HasIgnoreCase CommonOptions where+ ignoreCaseLens :: Lens' CommonOptions IgnoreCase+ ignoreCaseLens =+ lens+ commonOptionsIgnoreCase+ (\s a -> s {commonOptionsIgnoreCase = a})++instance HasRecursive CommonOptions where+ recursiveLens :: Lens' CommonOptions Recursive+ recursiveLens =+ lens+ commonOptionsRecursive+ (\s a -> s {commonOptionsRecursive = a})++instance HasRawRegex CommonOptions where+ rawRegexLens :: Lens' CommonOptions RawRegex+ rawRegexLens =+ lens+ commonOptionsRawRegex+ (\s a -> s {commonOptionsRawRegex = a})++instance HasInputFilenames CommonOptions where+ inputFilenamesLens :: Lens' CommonOptions [InputFilename]+ inputFilenamesLens =+ lens+ commonOptionsInputFilenames+ (\s a -> s {commonOptionsInputFilenames = a})++commonOptionsParser :: Parser CommonOptions+commonOptionsParser =+ CommonOptions+ <$> ignoreCaseParser+ <*> recursiveParser+ <*> rawRegexParser+ <*> inputFilenamesParser++-- | A default set of 'CommonOptions'. Defined as the following:+--+-- >>> :{+-- let opts =+-- CommonOptions+-- { commonOptionsIgnoreCase = DoNotIgnoreCase+-- , commonOptionsRecursive = NotRecursive+-- , commonOptionsRawRegex = RawRegex { unRawRegex = "" }+-- , commonOptionsInputFilenames = []+-- }+-- :}+--+-- >>> opts == defaultCommonOptions+-- True+defaultCommonOptions :: CommonOptions+defaultCommonOptions =+ CommonOptions DoNotIgnoreCase NotRecursive (RawRegex "") []
+ src/Highlight/Highlight.hs view
@@ -0,0 +1,35 @@+{- |+Module : Highlight.Highlight++Copyright : Dennis Gosnell 2017+License : BSD3++Maintainer : Dennis Gosnell (cdep.illabout@gmail.com)+Stability : experimental+Portability : unknown++This module contains the 'defaultMain' function for the @highlight@ program.+-}++module Highlight.Highlight where++import Data.Monoid ((<>))+import Options.Applicative+ (InfoMod, ParserInfo, (<**>), execParser, fullDesc, helper,+ info, progDesc)++import Highlight.Highlight.Options (Options, optionsParser)+import Highlight.Highlight.Run (run)++defaultMain :: IO ()+defaultMain = do+ options <- execParser parserInfo+ run options+ where+ parserInfo :: ParserInfo Options+ parserInfo = info (optionsParser <**> helper) infoMod++ infoMod :: InfoMod a+ infoMod =+ fullDesc <>+ progDesc "Highlight PATTERN in a each FILE or standard input"
+ src/Highlight/Highlight/Monad.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE Rank2Types #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Highlight.Highlight.Monad+ ( module Highlight.Highlight.Monad+ , module Highlight.Common.Monad+ ) where++import Prelude ()+import Prelude.Compat++import Control.Lens (view)+import Control.Monad.Reader (MonadReader)+import Control.Monad.State (MonadState, get)+import Data.ByteString (ByteString)++import Highlight.Common.Error (HighlightErr(..))+import Highlight.Common.Monad+ (CommonHighlightM,+ FilenameHandlingFromFiles(NoFilename, PrintFilename), InputData,+ Output, compileHighlightRegexWithErr, createInputData,+ getInputFilenamesM, getRecursiveM, handleInputData,+ runCommonHighlightM, runOutputProducer)+import Highlight.Highlight.Options+ (ColorGrepFilenames, HasColorGrepFilenames(colorGrepFilenamesLens),+ Options(..))+import Highlight.Util (modify')++-- | The internal state that is used to figure out how to color filenames from+-- @grep@.+data FromGrepFilenameState = FromGrepFilenameState+ { fromGrepFilenameStatePrevFileNum :: {-# UNPACK #-} !Int+ , fromGrepFilenameStatePrevFilename :: !(Maybe ByteString)+ }++initFromGrepFilenameState :: FromGrepFilenameState+initFromGrepFilenameState =+ FromGrepFilenameState+ { fromGrepFilenameStatePrevFileNum = (-1)+ , fromGrepFilenameStatePrevFilename = Nothing+ }++-- | Call 'updateFilename' and return the new file number after doing the+-- update.+updateFilenameM :: MonadState FromGrepFilenameState m => ByteString -> m Int+updateFilenameM nextFilename = do+ modify' $ updateFilename nextFilename+ FromGrepFilenameState newFileNum _ <- get+ return newFileNum++-- | Update the file number in 'FromGrepFilenameState' if the 'ByteString'+-- filename passed in is different from that in 'FromGrepFilenameState'.+updateFilename :: ByteString -> FromGrepFilenameState -> FromGrepFilenameState+updateFilename nextFilename (FromGrepFilenameState prevFileNum prevFilename)+ | Just nextFilename == prevFilename =+ FromGrepFilenameState prevFileNum prevFilename+ | otherwise =+ FromGrepFilenameState (prevFileNum + 1) (Just nextFilename)++-------------------------+-- The Highlight Monad --+-------------------------++-- | 'HighlightM' is just 'CommonHighlightM' specialized for @highlight@.+type HighlightM = CommonHighlightM Options FromGrepFilenameState HighlightErr++runHighlightM :: Options -> HighlightM a -> IO (Either HighlightErr a)+runHighlightM opts = runCommonHighlightM opts initFromGrepFilenameState++----------------------------------+-- Get value of certain options --+----------------------------------++-- | Get the value of the 'ColorGrepFilenames' option.+getColorGrepFilenamesM+ :: (HasColorGrepFilenames r, MonadReader r m) => m ColorGrepFilenames+getColorGrepFilenamesM = view colorGrepFilenamesLens
+ src/Highlight/Highlight/Options.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE OverloadedStrings #-}++module Highlight.Highlight.Options+ ( ColorGrepFilenames(..)+ , colorGrepFilenamesParser+ , HasColorGrepFilenames(..)+ , Options(..)+ , defaultOptions+ , optionsParser+ , HasOptions(..)+ , module Highlight.Common.Options+ ) where++import Prelude ()+import Prelude.Compat++import Control.Lens (Lens', lens)+import Data.Monoid ((<>))+import Options.Applicative (Parser, flag, help, long, short)++import Highlight.Common.Options+ (CommonOptions, HasCommonOptions(commonOptionsLens),+ HasIgnoreCase(ignoreCaseLens), HasInputFilenames(inputFilenamesLens),+ HasRawRegex(rawRegexLens), HasRecursive(recursiveLens),+ IgnoreCase(DoNotIgnoreCase, IgnoreCase),+ InputFilename(InputFilename, unInputFilename), RawRegex(RawRegex),+ Recursive(Recursive), commonOptionsParser, defaultCommonOptions)++--------------------------+-- Color grep filenames --+--------------------------++-- | Whether or not to color filenames output by @grep@ when reading in from+-- stdin.+data ColorGrepFilenames = ColorGrepFilenames | DoNotColorGrepFileNames+ deriving (Eq, Read, Show)++class HasColorGrepFilenames r where+ colorGrepFilenamesLens :: Lens' r ColorGrepFilenames++colorGrepFilenamesParser :: Parser ColorGrepFilenames+colorGrepFilenamesParser =+ let mods =+ long "from-grep" <>+ short 'g' <>+ help "highlight output from grep"+ in flag DoNotColorGrepFileNames ColorGrepFilenames mods++-------------+-- Options --+-------------++data Options = Options+ { optionsColorGrepFilenames :: ColorGrepFilenames+ , optionsCommonOptions :: CommonOptions+ } deriving (Eq, Read, Show)++class HasOptions r where+ optionsLens :: Lens' r Options++instance HasOptions Options where+ optionsLens :: Lens' Options Options+ optionsLens = id++instance HasColorGrepFilenames Options where+ colorGrepFilenamesLens :: Lens' Options ColorGrepFilenames+ colorGrepFilenamesLens =+ lens+ optionsColorGrepFilenames+ (\s a -> s {optionsColorGrepFilenames = a})++instance HasCommonOptions Options where+ commonOptionsLens :: Lens' Options CommonOptions+ commonOptionsLens =+ lens+ optionsCommonOptions+ (\s a -> s {optionsCommonOptions = a})++instance HasIgnoreCase Options+instance HasRecursive Options+instance HasRawRegex Options+instance HasInputFilenames Options++optionsParser :: Parser Options+optionsParser =+ Options+ <$> colorGrepFilenamesParser+ <*> commonOptionsParser++defaultOptions :: Options+defaultOptions = Options DoNotColorGrepFileNames defaultCommonOptions
+ src/Highlight/Highlight/Run.hs view
@@ -0,0 +1,125 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}++module Highlight.Highlight.Run where++import Prelude ()+import Prelude.Compat++import Control.Exception (IOException)+import Control.Monad.Reader (MonadReader)+import Control.Monad.State (MonadState)+import Data.ByteString (ByteString, empty)+import qualified Data.ByteString.Char8+import Data.Monoid ((<>))+import Pipes (Producer)+import Text.RE.PCRE (RE, (*=~))+import Text.RE.Replace (replaceAll)++import Highlight.Common.Color+ (colorForFileNumber, colorReset, colorVividRedBold,+ colorVividWhiteBold)+import Highlight.Common.Error (handleErr)+import Highlight.Highlight.Monad+ (FilenameHandlingFromFiles(..), FromGrepFilenameState, HighlightM,+ Output, compileHighlightRegexWithErr, createInputData,+ getColorGrepFilenamesM, getInputFilenamesM, getRecursiveM,+ handleInputData, runHighlightM, runOutputProducer, updateFilenameM)+import Highlight.Highlight.Options+ (HasColorGrepFilenames,+ ColorGrepFilenames(ColorGrepFilenames, DoNotColorGrepFileNames),+ Options(..))+import Highlight.Pipes (stdinLines)++run :: Options -> IO ()+run opts = do+ eitherRes <- runHighlightM opts prog+ either handleErr return eitherRes++prog :: HighlightM ()+prog = do+ outputProducer <- highlightOutputProducer stdinLines+ runOutputProducer outputProducer++highlightOutputProducer+ :: Producer ByteString HighlightM ()+ -> HighlightM (Producer Output HighlightM ())+highlightOutputProducer stdinProducer = do+ regex <- compileHighlightRegexWithErr+ inputFilenames <- getInputFilenamesM+ recursive <- getRecursiveM+ inputData <- createInputData recursive inputFilenames stdinProducer+ let outputProducer =+ handleInputData+ (handleStdinInput regex)+ (handleFileInput regex)+ handleError+ inputData+ return outputProducer++handleStdinInput+ :: ( HasColorGrepFilenames r+ , MonadState FromGrepFilenameState m+ , MonadReader r m+ )+ => RE -> ByteString -> m [ByteString]+handleStdinInput regex input = do+ colorGrepFilenames <- getColorGrepFilenamesM+ case colorGrepFilenames of+ DoNotColorGrepFileNames -> return $ formatNormalLine regex input+ ColorGrepFilenames -> do+ let (beforeColon, colonAndAfter) =+ Data.ByteString.Char8.break (== ':') input+ if colonAndAfter == empty+ then return $ formatNormalLine regex input+ else do+ let filePath = beforeColon+ lineWithoutColon = Data.ByteString.Char8.drop 1 colonAndAfter+ fileNumber <- updateFilenameM filePath+ return $+ formatLineWithFilename regex fileNumber filePath lineWithoutColon++formatLineWithFilename+ :: RE -> Int -> ByteString -> ByteString -> [ByteString]+formatLineWithFilename regex fileNumber filePath input =+ [ colorForFileNumber fileNumber+ , filePath+ , colorVividWhiteBold+ , ": "+ , colorReset+ , highlightMatchInRed regex input+ ]++formatNormalLine :: RE -> ByteString -> [ByteString]+formatNormalLine regex input =+ [highlightMatchInRed regex input]++handleFileInput+ :: Monad m+ => RE+ -> FilenameHandlingFromFiles+ -> ByteString+ -> Int+ -> ByteString+ -> m [ByteString]+handleFileInput regex NoFilename _ _ input =+ return $ formatNormalLine regex input+handleFileInput regex PrintFilename filePath fileNumber input =+ return $ formatLineWithFilename regex fileNumber filePath input++handleError+ :: Monad m+ => ByteString -> IOException -> Maybe IOException -> m [ByteString]+handleError filePath _ (Just _) =+ return ["Error when trying to read file or directory \"", filePath , "\""]+handleError filePath _ Nothing =+ return ["Error when trying to read file \"", filePath , "\""]++highlightMatchInRed :: RE -> ByteString -> ByteString+highlightMatchInRed regex input =+ let matches = input *=~ regex+ in replaceAll replaceInRedByteString matches++replaceInRedByteString :: ByteString+replaceInRedByteString = colorVividRedBold <> "$0" <> colorReset
+ src/Highlight/Hrep.hs view
@@ -0,0 +1,23 @@++module Highlight.Hrep where++import Data.Monoid ((<>))+import Options.Applicative+ (InfoMod, ParserInfo, (<**>), execParser, fullDesc, helper, info,+ progDesc)++import Highlight.Common.Options (CommonOptions, commonOptionsParser)+import Highlight.Hrep.Run (run)++defaultMain :: IO ()+defaultMain = do+ options <- execParser parserInfo+ run options+ where+ parserInfo :: ParserInfo CommonOptions+ parserInfo = info (commonOptionsParser <**> helper) infoMod++ infoMod :: InfoMod a+ infoMod =+ fullDesc <>+ progDesc "Search for PATTERN in each FILE or standard input."
+ src/Highlight/Hrep/Monad.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Highlight.Hrep.Monad+ ( module Highlight.Hrep.Monad+ , module Highlight.Common.Monad+ ) where++import Prelude ()+import Prelude.Compat++import Highlight.Common.Error (HighlightErr(..))+import Highlight.Common.Monad+ (CommonHighlightM,+ FilenameHandlingFromFiles(NoFilename, PrintFilename), InputData,+ Output, compileHighlightRegexWithErr, createInputData,+ getInputFilenamesM, getRecursiveM, handleInputData,+ runCommonHighlightM, runOutputProducer)+import Highlight.Common.Options (CommonOptions)++--------------------+-- The Hrep Monad --+--------------------++-- | 'HrepM' is just 'CommonHighlightM' specialized for @hrep@.+type HrepM = CommonHighlightM CommonOptions () HighlightErr++runHrepM :: CommonOptions -> HrepM a -> IO (Either HighlightErr a)+runHrepM opts = runCommonHighlightM opts ()
+ src/Highlight/Hrep/Run.hs view
@@ -0,0 +1,112 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}++module Highlight.Hrep.Run where++import Prelude ()+import Prelude.Compat++import Control.Exception (IOException)+import Data.ByteString (ByteString)+import Data.Maybe (maybeToList)+import Data.Monoid ((<>))+import Pipes (Producer)+import Text.RE.PCRE (RE, (*=~), anyMatches)+import Text.RE.Replace (replaceAll)++import Highlight.Common.Color+ (colorForFileNumber, colorReset, colorVividRedBold,+ colorVividWhiteBold)+import Highlight.Common.Error (handleErr)+import Highlight.Common.Options (CommonOptions(..))+import Highlight.Hrep.Monad+ (FilenameHandlingFromFiles(..), HrepM, Output,+ compileHighlightRegexWithErr, createInputData, getInputFilenamesM,+ getRecursiveM, handleInputData, runHrepM, runOutputProducer)+import Highlight.Pipes (stdinLines)++run :: CommonOptions -> IO ()+run opts = do+ eitherRes <- runHrepM opts prog+ either handleErr return eitherRes++prog :: HrepM ()+prog = do+ outputProducer <- hrepOutputProducer stdinLines+ runOutputProducer outputProducer++hrepOutputProducer+ :: Producer ByteString HrepM ()+ -> HrepM (Producer Output HrepM ())+hrepOutputProducer stdinProducer = do+ regex <- compileHighlightRegexWithErr+ inputFilenames <- getInputFilenamesM+ recursive <- getRecursiveM+ inputData <- createInputData recursive inputFilenames stdinProducer+ let outputProducer =+ handleInputData+ (handleStdinInput regex)+ (handleFileInput regex)+ handleError+ inputData+ return outputProducer++handleStdinInput+ :: Monad m+ => RE -> ByteString -> m [ByteString]+handleStdinInput regex input =+ return $ formatNormalLine regex input++formatNormalLine :: RE -> ByteString -> [ByteString]+formatNormalLine regex =+ maybeToList . highlightMatchInRed regex++handleFileInput+ :: Monad m+ => RE+ -> FilenameHandlingFromFiles+ -> ByteString+ -> Int+ -> ByteString+ -> m [ByteString]+handleFileInput regex NoFilename _ _ input =+ return $ formatNormalLine regex input+handleFileInput regex PrintFilename filePath fileNumber input =+ return $ formatLineWithFilename regex fileNumber filePath input++formatLineWithFilename+ :: RE -> Int -> ByteString -> ByteString -> [ByteString]+formatLineWithFilename regex fileNumber filePath input =+ case highlightMatchInRed regex input of+ Nothing -> []+ Just line ->+ [ colorForFileNumber fileNumber+ , filePath+ , colorVividWhiteBold+ , ": "+ , colorReset+ , line+ ]++handleError+ :: Monad m+ => ByteString+ -> IOException+ -> Maybe IOException+ -> m [ByteString]+handleError filePath _ (Just _) =+ return ["Error when trying to read file or directory \"", filePath, "\""]+handleError filePath _ Nothing =+ return ["Error when trying to read file \"", filePath, "\""]++highlightMatchInRed :: RE -> ByteString -> Maybe ByteString+highlightMatchInRed regex input =+ let matches = input *=~ regex+ didMatch = anyMatches matches+ in if didMatch+ then Just $ replaceAll replaceInRedByteString matches+ else Nothing++replaceInRedByteString :: ByteString+replaceInRedByteString = colorVividRedBold <> "$0" <> colorReset
+ src/Highlight/Pipes.hs view
@@ -0,0 +1,154 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Highlight.Pipes where++import Prelude ()+import Prelude.Compat++import Control.Exception (throwIO, try)+import Control.Monad.IO.Class (MonadIO(liftIO))+import Data.ByteString (ByteString, hGetLine, hPutStr)+import Foreign.C.Error (Errno(Errno), ePIPE)+import GHC.IO.Exception+ (IOException(IOError), IOErrorType(ResourceVanished), ioe_errno,+ ioe_type)+import Pipes (Consumer', Producer', Proxy, await, each, yield)+import System.Directory (getDirectoryContents)+import System.FilePath ((</>))+import System.IO (Handle, stderr, stdin)++import Highlight.Util+ (closeHandleIfEOFOrThrow, openFilePathForReading)++-- | Read input from a 'Handle', split it into lines, and return each of those+-- lines as a 'ByteString' in a 'Producer'.+--+-- This function will close the 'Handle' if the end of the file is reached.+-- However, if an error was thrown while reading input from the 'Handle', the+-- 'Handle' is not closed.+--+-- Setup for examples:+--+-- >>> import Pipes.Prelude (toListM)+-- >>> import System.IO (IOMode(ReadMode), openBinaryFile)+-- >>> let goodFilePath = "test/golden/test-files/file2"+--+-- Examples:+--+-- >>> handle <- openBinaryFile goodFilePath ReadMode+-- >>> fmap head . toListM $ fromHandleLines handle+-- "Proud Pour is a wine company that funds solutions to local environmental"+fromHandleLines :: forall m. MonadIO m => Handle -> Producer' ByteString m ()+fromHandleLines handle = go+ where+ go :: Producer' ByteString m ()+ go = do+ eitherLine <- liftIO . try $ hGetLine handle+ case eitherLine of+ Left ioerr -> closeHandleIfEOFOrThrow handle ioerr+ Right line -> yield line *> go+{-# INLINABLE fromHandleLines #-}++-- | Call 'fromHandleLines' on 'stdin'.+stdinLines :: forall m. MonadIO m => Producer' ByteString m ()+stdinLines = fromHandleLines stdin+{-# INLINABLE stdinLines #-}++-- | Try calling 'fromHandleLines' on the 'Handle' obtained from+-- 'openFilePathForReading'.+--+-- Setup for examples:+--+-- >>> import Pipes (Producer)+-- >>> import Pipes.Prelude (toListM)+--+-- >>> let t a = a :: IO (Either IOException (Producer ByteString IO ()))+-- >>> let goodFilePath = "test/golden/test-files/file2"+-- >>> let badFilePath = "thisfiledoesnotexist"+-- >>> let handleErr err = error $ "got following error: " `mappend` show err+--+-- Example:+--+-- >>> eitherProducer <- t $ fromFileLines goodFilePath+-- >>> let producer = either handleErr id eitherProducer+-- >>> fmap head $ toListM producer+-- "Proud Pour is a wine company that funds solutions to local environmental"+--+-- Returns 'IOException' if there was an error when opening the file.+--+-- >>> eitherProducer <- t $ fromFileLines badFilePath+-- >>> either print (const $ return ()) eitherProducer+-- thisfiledoesnotexist: openBinaryFile: does not exist ...+fromFileLines+ :: forall m n x' x.+ (MonadIO m, MonadIO n)+ => FilePath+ -> m (Either IOException (Proxy x' x () ByteString n ()))+fromFileLines filePath = do+ eitherHandle <- openFilePathForReading filePath+ case eitherHandle of+ Left ioerr -> return $ Left ioerr+ Right handle -> return . Right $ fromHandleLines handle+{-# INLINABLE fromFileLines #-}++-- | Output 'ByteString's to 'stderr'.+--+-- If an 'ePIPE' error is thrown, then just 'return' @()@. If any other error+-- is thrown, rethrow the error.+--+-- Setup for examples:+--+-- >>> :set -XOverloadedStrings+-- >>> import Pipes ((>->), runEffect)+--+-- Example:+--+-- >>> runEffect $ yield "hello" >-> stderrConsumer+-- hello+stderrConsumer :: forall m. MonadIO m => Consumer' ByteString m ()+stderrConsumer = go+ where+ go :: Consumer' ByteString m ()+ go = do+ bs <- await+ x <- liftIO $ try (hPutStr stderr bs)+ case x of+ Left IOError { ioe_type = ResourceVanished, ioe_errno = Just ioe }+ | Errno ioe == ePIPE -> return ()+ Left e -> liftIO $ throwIO e+ Right () -> go+{-# INLINABLE stderrConsumer #-}++-- | Select all immediate children of the given directory, ignoring @\".\"@ and+-- @\"..\"@.+--+-- Throws an 'IOException' if the directory is not readable or (on Windows) if+-- the directory is actually a reparse point.+--+-- Setup for examples:+--+-- >>> import Data.List (sort)+-- >>> import Pipes.Prelude (toListM)+--+-- Examples:+--+-- >>> fmap (head . sort) . toListM $ childOf "test/golden/test-files"+-- "test/golden/test-files/dir1"+--+-- TODO: This could be rewritten to be faster by using the Windows- and+-- Linux-specific functions to only read one file from a directory at a time+-- like the actual+-- <https://hackage.haskell.org/package/dirstream-1.0.3/docs/Data-DirStream.html#v:childOf childOf>+-- function.+childOf :: MonadIO m => FilePath -> Producer' FilePath m ()+childOf path = do+ files <- liftIO $ getDirectoryContents path+ let filteredFiles = filter isNormalFile files+ fullFiles = fmap (path </>) filteredFiles+ each fullFiles+ where+ isNormalFile :: FilePath -> Bool+ isNormalFile file = file /= "." && file /= ".."+{-# INLINABLE childOf #-}
+ src/Highlight/Util.hs view
@@ -0,0 +1,103 @@+module Highlight.Util where++import Prelude ()+import Prelude.Compat++import Control.Exception (IOException, try)+import Control.Monad.IO.Class (MonadIO(liftIO))+import Control.Monad.State (MonadState, get, put)+import Data.ByteString (ByteString)+import Data.ByteString.Unsafe (unsafePackMallocCStringLen)+import Data.Semigroup (Semigroup, (<>))+import Foreign.C (newCStringLen)+import System.Exit (ExitCode(ExitFailure), exitWith)+import System.IO (Handle, IOMode(ReadMode), hClose, hIsEOF, openBinaryFile)++-- | Convert a 'String' to a 'ByteString' with the encoding for the current+-- locale.+--+-- >>> convertStringToRawByteString "hello"+-- "hello"+convertStringToRawByteString :: MonadIO m => String -> m ByteString+convertStringToRawByteString str = liftIO $ do+ cStringLen <- newCStringLen str+ unsafePackMallocCStringLen cStringLen+{-# INLINABLE convertStringToRawByteString #-}++-- | Open a 'FilePath' in 'ReadMode'.+--+-- On success, return a 'Right' 'Handle':+--+-- >>> openFilePathForReading "README.md"+-- Right {handle: README.md}+--+-- On error, return a 'Left' 'IOException':+--+-- >>> openFilePathForReading "thisfiledoesntexist"+-- Left thisfiledoesntexist: openBinaryFile: does not exist ...+openFilePathForReading :: MonadIO m => FilePath -> m (Either IOException Handle)+openFilePathForReading filePath =+ liftIO . try $ openBinaryFile filePath ReadMode+{-# INLINABLE openFilePathForReading #-}++-- | Combine values in two 'Applicative's with '<>'.+--+-- >>> combineApplicatives (Just "hello") (Just " world")+-- Just "hello world"+--+-- >>> combineApplicatives (Just "hello") Nothing+-- Nothing+combineApplicatives :: (Applicative f, Semigroup a) => f a -> f a -> f a+combineApplicatives action1 action2 =+ (<>) <$> action1 <*> action2+{-# INLINABLE combineApplicatives #-}++-- | Handle an 'IOException' that occurs when reading from a 'Handle'. Check+-- if the 'IOException' is an EOF exception ('hIsEOF'). If so, then just close+-- the 'Handle'. Otherwise, throw the 'IOException' that is passed in.+closeHandleIfEOFOrThrow :: MonadIO m => Handle -> IOException -> m ()+closeHandleIfEOFOrThrow handle ioerr = liftIO $ do+ isEOF <- hIsEOF handle+ if isEOF+ then hClose handle+ else ioError ioerr+{-# INLINABLE closeHandleIfEOFOrThrow #-}++-- | Call 'exitWith' with 'ExitFailure'+--+-- >>> die 10 "message"+-- ERROR: message+-- *** Exception: ExitFailure 10+die+ :: Int -- ^ exit code+ -> String -- ^ error message to print to console+ -> IO a+die exitCode msg = do+ putStrLn $ "ERROR: " <> msg+ exitWith $ ExitFailure exitCode+{-# INLINABLE die #-}++-- | Perform an action when a list is non-null.+--+-- >>> whenNonNull [1,2,3] $ putStrLn "hello"+-- hello+-- >>> whenNonNull [] $ putStrLn "bye"+--+whenNonNull :: Monad m => [a] -> m () -> m ()+whenNonNull [] _ = return ()+whenNonNull _ action = action+{-# INLINABLE whenNonNull #-}+++-- | A variant of 'modify' in which the computation is strict in the+-- new state.+--+-- * @'modify'' f = 'get' >>= (('$!') 'put' . f)@+--+-- This is used because 'modify'' is not available in the @tranformers-0.3.0.0@+-- package.+modify' :: MonadState s m => (s -> s) -> m ()+modify' f = do+ s <- get+ put $! f s+{-# INLINE modify' #-}
+ test/DocTest.hs view
@@ -0,0 +1,36 @@++module Main (main) where++import Prelude++import Data.Monoid ((<>))+import Test.DocTest (doctest)++main :: IO ()+main = doctest $ ["src/"] <> ghcExtensions++ghcExtensions :: [String]+ghcExtensions =+ [+ -- "-XConstraintKinds"+ -- , "-XDataKinds"+ -- , "-XDeriveDataTypeable"+ -- , "-XDeriveGeneric"+ -- , "-XEmptyDataDecls"+ -- , "-XFlexibleContexts"+ -- , "-XFlexibleInstances"+ -- , "-XGADTs"+ -- , "-XGeneralizedNewtypeDeriving"+ -- , "-XInstanceSigs"+ -- , "-XMultiParamTypeClasses"+ -- , "-XNoImplicitPrelude"+ -- , "-XOverloadedStrings"+ -- , "-XPolyKinds"+ -- , "-XRankNTypes"+ -- , "-XRecordWildCards"+ -- , "-XScopedTypeVariables"+ -- , "-XStandaloneDeriving"+ -- , "-XTupleSections"+ -- , "-XTypeFamilies"+ -- , "-XTypeOperators"+ ]
+ test/Test.hs view
@@ -0,0 +1,19 @@+module Main where++import Prelude ()+import Prelude.Compat++import Test.Tasty (TestTree, defaultMain, testGroup)++import Test.Golden (goldenTests)++main :: IO ()+main = do+ -- createDirectoryIfMissing False (testDir </> "empty-dir")+ tests <- testsIO+ defaultMain tests++testsIO :: IO TestTree+testsIO = do+ return $ testGroup "tests" [goldenTests]+
+ test/Test/Golden.hs view
@@ -0,0 +1,310 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}++module Test.Golden where++import Prelude ()+import Prelude.Compat++import Control.Exception (try)+import Control.Lens ((&), (.~))+import Control.Monad.IO.Class (MonadIO)+import Data.ByteString (ByteString)+import Data.ByteString.Lazy (fromStrict)+import qualified Data.ByteString.Lazy as LByteString+import Data.Foldable (fold)+import Data.Monoid ((<>))+import Pipes (Pipe, Producer, (>->))+import Pipes.Prelude (mapFoldable, toListM)+import System.Directory+ (Permissions, emptyPermissions, removeFile, setOwnerReadable,+ setOwnerSearchable, setOwnerWritable, setPermissions)+import System.IO (IOMode(WriteMode), hClose, openBinaryFile)+import System.IO.Error (isPermissionError)+import Test.Tasty (TestTree, testGroup, withResource)+import Test.Tasty.Golden (goldenVsString)++import Highlight.Common.Monad (Output(OutputStderr, OutputStdout))+import Highlight.Common.Options+ (CommonOptions, IgnoreCase(IgnoreCase), Recursive(Recursive),+ defaultCommonOptions, ignoreCaseLens, inputFilenamesLens,+ rawRegexLens, recursiveLens)+import Highlight.Highlight.Monad (HighlightM, runHighlightM)+import Highlight.Highlight.Options+ (ColorGrepFilenames(ColorGrepFilenames), Options,+ colorGrepFilenamesLens, defaultOptions)+import Highlight.Highlight.Run (highlightOutputProducer)+import Highlight.Hrep.Monad (HrepM, runHrepM)+import Highlight.Hrep.Run (hrepOutputProducer)+import Highlight.Pipes (fromFileLines, stdinLines)++runHighlightTestWithStdin+ :: Options+ -> Producer ByteString HighlightM ()+ -> (forall m. Monad m => Pipe Output ByteString m ())+ -> IO LByteString.ByteString+runHighlightTestWithStdin opts stdinPipe filterPipe = do+ eitherByteStrings <- runHighlightM opts $ do+ outputProducer <- highlightOutputProducer stdinPipe+ toListM $ outputProducer >-> filterPipe+ case eitherByteStrings of+ Left err -> error $ "unexpected error: " <> show err+ Right byteStrings -> return . fromStrict $ fold byteStrings++runHighlightTest+ :: Options+ -> (forall m. Monad m => Pipe Output ByteString m ())+ -> IO LByteString.ByteString+runHighlightTest opts =+ runHighlightTestWithStdin opts stdinLines++runHrepTestWithStdin+ :: CommonOptions+ -> (Producer ByteString HrepM ())+ -> (forall m. Monad m => Pipe Output ByteString m ())+ -> IO LByteString.ByteString+runHrepTestWithStdin opts stdinPipe filterPipe = do+ eitherByteStrings <- runHrepM opts $ do+ outputProducer <- hrepOutputProducer stdinPipe+ toListM $ outputProducer >-> filterPipe+ case eitherByteStrings of+ Left err -> error $ "unexpected error: " <> show err+ Right byteStrings -> return . fromStrict $ fold byteStrings++runHrepTest+ :: CommonOptions+ -> (forall m. Monad m => Pipe Output ByteString m ())+ -> IO LByteString.ByteString+runHrepTest opts =+ runHrepTestWithStdin opts stdinLines++filterStdout :: Monad m => Pipe Output ByteString m ()+filterStdout = mapFoldable f+ where+ f :: Output -> Maybe ByteString+ f (OutputStderr _) = Nothing+ f (OutputStdout byteString) = Just byteString++filterStderr :: Monad m => Pipe Output ByteString m ()+filterStderr = mapFoldable f+ where+ f :: Output -> Maybe ByteString+ f (OutputStderr byteString) = Just byteString+ f (OutputStdout _) = Nothing++getFileOutputProducer+ :: MonadIO m+ => FilePath -> IO (Producer ByteString m ())+getFileOutputProducer filePath = do+ eitherProducer <- fromFileLines filePath+ case eitherProducer of+ Left ioerr ->+ error $+ "ERROR: following error occured when trying to read \"" <>+ filePath <> "\": " <> show ioerr+ Right producer -> return producer++testStderrAndStdout+ :: String+ -> FilePath+ -> ( ( forall m. Monad m => Pipe Output ByteString m ())+ -> IO LByteString.ByteString+ )+ -> TestTree+testStderrAndStdout msg path runner =+ testGroup+ msg+ [ goldenVsString "stderr" (path <> ".stderr") (runner filterStderr)+ , goldenVsString "stdout" (path <> ".stdout") (runner filterStdout)+ ]++goldenTests :: TestTree+goldenTests =+ withResource createUnreadableFile (const deleteUnreadableFile) $+ const (testGroup "golden tests" [highlightGoldenTests, hrepGoldenTests])++createUnreadableFile :: IO ()+createUnreadableFile = do+ eitherHandle <- try $ openBinaryFile unreadableFilePath WriteMode+ case eitherHandle of+ Right handle -> do+ hClose handle+ makeFileUnreadable unreadableFilePath+ Left ioerr+ | isPermissionError ioerr ->+ -- assume that the file already exists, and just try to make sure that+ -- the permissions are null+ makeFileUnreadable unreadableFilePath+ | otherwise ->+ -- we shouldn't have gotten an error here, so just rethrow it+ ioError ioerr++makeFileUnreadable :: FilePath -> IO ()+makeFileUnreadable filePath = setPermissions filePath emptyPermissions++makeFileReadable :: FilePath -> IO ()+makeFileReadable filePath =+ setPermissions filePath fullPermissions+ where+ fullPermissions :: Permissions+ fullPermissions =+ setOwnerWritable True .+ setOwnerSearchable True .+ setOwnerReadable True $+ emptyPermissions++deleteUnreadableFile :: IO ()+deleteUnreadableFile = do+ makeFileReadable unreadableFilePath+ eitherRes <- try $ removeFile unreadableFilePath+ either ioError return eitherRes++unreadableFilePath :: FilePath+unreadableFilePath = "test/golden/test-files/dir2/unreadable-file"++---------------------+-- Highlight Tests --+---------------------++highlightGoldenTests :: TestTree+highlightGoldenTests =+ testGroup+ "highlight"+ [ testHighlightSingleFile+ , testHighlightMultiFile+ , testHighlightFromGrep+ ]++testHighlightSingleFile :: TestTree+testHighlightSingleFile =+ let opts =+ defaultOptions+ & rawRegexLens .~ "or"+ & inputFilenamesLens .~ ["test/golden/test-files/file1"]+ in testStderrAndStdout+ "`highlight or 'test/golden/test-files/file1'`"+ "test/golden/golden-files/highlight/single-file"+ (runHighlightTest opts)++testHighlightMultiFile :: TestTree+testHighlightMultiFile =+ let opts =+ defaultOptions+ & rawRegexLens .~ "and"+ & ignoreCaseLens .~ IgnoreCase+ & recursiveLens .~ Recursive+ & inputFilenamesLens .~+ [ "test/golden/test-files/dir1"+ , "test/golden/test-files/empty-file"+ , "test/golden/test-files/dir2"+ ]+ testName =+ "`touch 'test/golden/test-files/dir2/unreadable-file' ; " <>+ "chmod 0 'test/golden/test-files/dir2/unreadable-file' ; " <>+ "highlight --ignore-case --recursive and " <>+ "'test/golden/test-files/dir1' " <>+ "'test/golden/test-files/empty-file' " <>+ "'test/golden/test-files/dir2' ; " <>+ "rm -rf 'test/golden/test-files/dir2/unreadable-file'`"+ in testStderrAndStdout+ testName+ "test/golden/golden-files/highlight/multi-file"+ (runHighlightTest opts)++testHighlightFromGrep :: TestTree+testHighlightFromGrep =+ let opts =+ defaultOptions+ & rawRegexLens .~ "and"+ & colorGrepFilenamesLens .~ ColorGrepFilenames+ testName =+ "`cat test/golden/test-files/from-grep | " <>+ "highlight --from-grep and`"+ in testStderrAndStdout+ testName+ "test/golden/golden-files/highlight/from-grep"+ (go opts)+ where+ go+ :: Options+ -> (forall m. Monad m => Pipe Output ByteString m ())+ -> IO LByteString.ByteString+ go opts outputPipe = do+ -- This is the output file from @grep@ to use for the test+ -- 'testHighlightFromGrep'.+ --+ -- This file was created with the following command:+ -- > $ grep --recursive and 'test/golden/test-files/dir1'+ let grepOutputTestFile = "test/golden/test-files/from-grep"+ grepOutputProducer <- getFileOutputProducer grepOutputTestFile+ runHighlightTestWithStdin opts grepOutputProducer outputPipe++----------------+-- Hrep Tests --+----------------++hrepGoldenTests :: TestTree+hrepGoldenTests =+ testGroup+ "hrep"+ [ testHrepSingleFile+ , testHrepMultiFile+ , testHrepFromStdin+ ]++testHrepSingleFile :: TestTree+testHrepSingleFile =+ let opts =+ defaultCommonOptions+ & rawRegexLens .~ "another"+ & inputFilenamesLens .~ ["test/golden/test-files/file1"]+ in testStderrAndStdout+ "`hrep another 'test/golden/test-files/file1'`"+ "test/golden/golden-files/hrep/single-file"+ (runHrepTest opts)++testHrepMultiFile :: TestTree+testHrepMultiFile =+ let opts =+ defaultCommonOptions+ & rawRegexLens .~ "as"+ & ignoreCaseLens .~ IgnoreCase+ & recursiveLens .~ Recursive+ & inputFilenamesLens .~+ [ "test/golden/test-files/dir1"+ , "test/golden/test-files/empty-file"+ , "test/golden/test-files/dir2"+ ]+ testName =+ "`touch 'test/golden/test-files/dir2/unreadable-file' ; " <>+ "chmod 0 'test/golden/test-files/dir2/unreadable-file' ; " <>+ "hrep --ignore-case --recursive as " <>+ "'test/golden/test-files/dir1' " <>+ "'test/golden/test-files/empty-file' " <>+ "'test/golden/test-files/dir2' ; " <>+ "rm -rf 'test/golden/test-files/dir2/unreadable-file'`"+ in testStderrAndStdout+ testName+ "test/golden/golden-files/hrep/multi-file"+ (runHrepTest opts)++testHrepFromStdin :: TestTree+testHrepFromStdin =+ let opts =+ defaultCommonOptions & rawRegexLens .~ "co."+ stdinInputFile = "test/golden/test-files/file2"+ testName =+ "`cat '" <> stdinInputFile <> "' | hrep 'co.'`"+ in testStderrAndStdout+ testName+ "test/golden/golden-files/hrep/from-stdin"+ (go opts stdinInputFile)+ where+ go+ :: CommonOptions+ -> FilePath+ -> (forall m. Monad m => Pipe Output ByteString m ())+ -> IO LByteString.ByteString+ go opts stdinInputFile outputPipe = do+ stdinProducer <- getFileOutputProducer stdinInputFile+ runHrepTestWithStdin opts stdinProducer outputPipe
+ test/golden/golden-files/highlight/from-grep.stderr view
+ test/golden/golden-files/highlight/from-grep.stdout view
@@ -0,0 +1,14 @@+[1m[94mtest/golden/test-files/dir1/file3[1m[97m: [0mBr[1m[91mand[0mon is a small town [1m[91mand[0m civil parish in the English county of Suffolk. It+[1m[94mtest/golden/test-files/dir1/file3[1m[97m: [0mis in the Forest Heath local government district. Br[1m[91mand[0mon is located in the+[1m[94mtest/golden/test-files/dir1/file3[1m[97m: [0mBreckl[1m[91mand[0m area on the border of Suffolk with the adjoining county of Norfolk.+[1m[94mtest/golden/test-files/dir1/file3[1m[97m: [0mSurrounded by Forestry Commission [1m[91mand[0m agricultural l[1m[91mand[0m it is considered a+[1m[94mtest/golden/test-files/dir1/file3[1m[97m: [0mEnglish Place Names) the likely origin of the name is "Br[1m[91mand[0mon, usually 'hill+[1m[94mtest/golden/test-files/dir1/file3[1m[97m: [0mthe town, gradually exp[1m[91mand[0ming up [1m[91mand[0m along the rising ground of the river+[1m[92mtest/golden/test-files/dir1/subdir1/file5[1m[97m: [0mLaura Belém is a Brazilian artist [1m[91mand[0m lecturer. Her work has been exhibited in+[1m[92mtest/golden/test-files/dir1/subdir1/file5[1m[97m: [0mBrazil, Denmark, Canada, France, Japan, Italy [1m[91mand[0m the United Kingdom. Born+[1m[92mtest/golden/test-files/dir1/subdir1/file5[1m[97m: [0mCentral Saint Martins College of Art [1m[91mand[0m Design in London, UK in 2000. She has+[1m[92mtest/golden/test-files/dir1/subdir1/file5[1m[97m: [0mbeen awarded grants by bodies such as The Triangle Association in Brooklyn [1m[91mand[0m+[1m[96mtest/golden/test-files/dir1/file4[1m[97m: [0mArmstrong [1m[91mand[0m aka W. N. Armstrong, was the Attorney General of Hawaii during+[1m[96mtest/golden/test-files/dir1/file4[1m[97m: [0mworld tour in 1881. He was born in Lahaina on the isl[1m[91mand[0m of Maui, the third of+[1m[96mtest/golden/test-files/dir1/file4[1m[97m: [0mten children of missionaries Clarissa Chapman Armstrong [1m[91mand[0m Richard Armstrong,+[1m[96mtest/golden/test-files/dir1/file4[1m[97m: [0mwho later served as the second kahu (pastor) of Kawaiahaʻo Church, [1m[91mand[0m
+ test/golden/golden-files/highlight/multi-file.stderr view
@@ -0,0 +1,1 @@+Error when trying to read file or directory "test/golden/test-files/dir2/unreadable-file"
+ test/golden/golden-files/highlight/multi-file.stdout view
@@ -0,0 +1,29 @@+[1m[94mtest/golden/test-files/dir1/file3[1m[97m: [0mBr[1m[91mand[0mon is a small town [1m[91mand[0m civil parish in the English county of Suffolk. It+[1m[94mtest/golden/test-files/dir1/file3[1m[97m: [0mis in the Forest Heath local government district. Br[1m[91mand[0mon is located in the+[1m[94mtest/golden/test-files/dir1/file3[1m[97m: [0mBreckl[1m[91mand[0m area on the border of Suffolk with the adjoining county of Norfolk.+[1m[94mtest/golden/test-files/dir1/file3[1m[97m: [0mSurrounded by Forestry Commission [1m[91mand[0m agricultural l[1m[91mand[0m it is considered a+[1m[94mtest/golden/test-files/dir1/file3[1m[97m: [0mrural town. According to Eilert Ekwall (The Concise Oxford Dictionary of+[1m[94mtest/golden/test-files/dir1/file3[1m[97m: [0mEnglish Place Names) the likely origin of the name is "Br[1m[91mand[0mon, usually 'hill+[1m[94mtest/golden/test-files/dir1/file3[1m[97m: [0mwhere broom grows'", the earliest known spelling being in the 11th century when+[1m[94mtest/golden/test-files/dir1/file3[1m[97m: [0mthe town, gradually exp[1m[91mand[0ming up [1m[91mand[0m along the rising ground of the river+[1m[94mtest/golden/test-files/dir1/file3[1m[97m: [0mvalley, was called Bromdun.+[1m[92mtest/golden/test-files/dir1/file4[1m[97m: [0mWilliam Nevins Armstrong (March 10, 1835 – October 16, 1905), aka Nevins+[1m[92mtest/golden/test-files/dir1/file4[1m[97m: [0mArmstrong [1m[91mand[0m aka W. N. Armstrong, was the Attorney General of Hawaii during+[1m[92mtest/golden/test-files/dir1/file4[1m[97m: [0mthe reign of King David Kalākaua. He is most widely known outside of Hawaii for+[1m[92mtest/golden/test-files/dir1/file4[1m[97m: [0mthe book Around the World with a King, his insider account of King Kalākaua's+[1m[92mtest/golden/test-files/dir1/file4[1m[97m: [0mworld tour in 1881. He was born in Lahaina on the isl[1m[91mand[0m of Maui, the third of+[1m[92mtest/golden/test-files/dir1/file4[1m[97m: [0mten children of missionaries Clarissa Chapman Armstrong [1m[91mand[0m Richard Armstrong,+[1m[92mtest/golden/test-files/dir1/file4[1m[97m: [0mwho later served as the second kahu (pastor) of Kawaiahaʻo Church, [1m[91mand[0m+[1m[92mtest/golden/test-files/dir1/file4[1m[97m: [0msubsequently was appointed President of the Board of Education for the Kingdom+[1m[92mtest/golden/test-files/dir1/file4[1m[97m: [0mof Hawaii.+[1m[96mtest/golden/test-files/dir1/subdir1/file5[1m[97m: [0mLaura Belém is a Brazilian artist [1m[91mand[0m lecturer. Her work has been exhibited in+[1m[96mtest/golden/test-files/dir1/subdir1/file5[1m[97m: [0mBrazil, Denmark, Canada, France, Japan, Italy [1m[91mand[0m the United Kingdom. Born+[1m[96mtest/golden/test-files/dir1/subdir1/file5[1m[97m: [0m1974, Belo Horizonte (Brazil), Belém graduated with an MA degree in Fine Art at+[1m[96mtest/golden/test-files/dir1/subdir1/file5[1m[97m: [0mCentral Saint Martins College of Art [1m[91mand[0m Design in London, UK in 2000. She has+[1m[96mtest/golden/test-files/dir1/subdir1/file5[1m[97m: [0mbeen awarded grants by bodies such as The Triangle Association in Brooklyn [1m[91mand[0m+[1m[96mtest/golden/test-files/dir1/subdir1/file5[1m[97m: [0mThe Casa de Velázquez in Madrid.+[1m[95mtest/golden/test-files/dir2/file6[1m[97m: [0mBoyde may refer to: Danish orthography is the system used to write the Danish+[1m[95mtest/golden/test-files/dir2/file6[1m[97m: [0mlanguage. The oldest preserved examples of written Danish are in the Runic+[1m[95mtest/golden/test-files/dir2/file6[1m[97m: [0malphabet, but by the end of the High Middle Ages the Runes had mostly been+[1m[95mtest/golden/test-files/dir2/file6[1m[97m: [0mreplaced by the Latin letters. Danish currently uses a 29-letter Latin-script+[1m[95mtest/golden/test-files/dir2/file6[1m[97m: [0malphabet, identical to the Norwegian alphabet.
+ test/golden/golden-files/highlight/single-file.stderr view
+ test/golden/golden-files/highlight/single-file.stdout view
@@ -0,0 +1,5 @@+The parallel novel is a piece of literature written within, derived from, [1m[91mor[0m+taking place during, the framew[1m[91mor[0mk of another w[1m[91mor[0mk of fiction by the same [1m[91mor[0m+another auth[1m[91mor[0m. Parallel novels [1m[91mor[0m "reimagined classics" are w[1m[91mor[0mks of fiction+that "b[1m[91mor[0mrow a character and fill in his st[1m[91mor[0my, mirr[1m[91mor[0m an 'old' plot, [1m[91mor[0m blend+the characters of one book with those of another".
+ test/golden/golden-files/hrep/from-stdin.stderr view
+ test/golden/golden-files/hrep/from-stdin.stdout view
@@ -0,0 +1,3 @@+Proud Pour is a wine [1m[91mcom[0mpany that funds solutions to local environmental+restoration of 100 wild oysters per bottle to New York Harbor and [1m[91mcoa[0mstal+and the Massachusetts Oyster Project. The [1m[91mcom[0mpany was founded by Berlin Crystal
+ test/golden/golden-files/hrep/multi-file.stderr view
@@ -0,0 +1,1 @@+Error when trying to read file or directory "test/golden/test-files/dir2/unreadable-file"
+ test/golden/golden-files/hrep/multi-file.stdout view
@@ -0,0 +1,9 @@+[1m[94mtest/golden/test-files/dir1/file3[1m[97m: [0mvalley, w[1m[91mas[0m called Bromdun.+[1m[92mtest/golden/test-files/dir1/file4[1m[97m: [0mArmstrong and aka W. N. Armstrong, w[1m[91mas[0m the Attorney General of Hawaii during+[1m[92mtest/golden/test-files/dir1/file4[1m[97m: [0mworld tour in 1881. He w[1m[91mas[0m born in Lahaina on the island of Maui, the third of+[1m[92mtest/golden/test-files/dir1/file4[1m[97m: [0mwho later served [1m[91mas[0m the second kahu (p[1m[91mas[0mtor) of Kawaiahaʻo Church, and+[1m[92mtest/golden/test-files/dir1/file4[1m[97m: [0msubsequently w[1m[91mas[0m appointed President of the Board of Education for the Kingdom+[1m[96mtest/golden/test-files/dir1/subdir1/file5[1m[97m: [0mLaura Belém is a Brazilian artist and lecturer. Her work h[1m[91mas[0m been exhibited in+[1m[96mtest/golden/test-files/dir1/subdir1/file5[1m[97m: [0mCentral Saint Martins College of Art and Design in London, UK in 2000. She h[1m[91mas[0m+[1m[96mtest/golden/test-files/dir1/subdir1/file5[1m[97m: [0mbeen awarded grants by bodies such [1m[91mas[0m The Triangle [1m[91mAs[0msociation in Brooklyn and+[1m[96mtest/golden/test-files/dir1/subdir1/file5[1m[97m: [0mThe C[1m[91mas[0ma de Velázquez in Madrid.
+ test/golden/golden-files/hrep/single-file.stderr view
+ test/golden/golden-files/hrep/single-file.stdout view
@@ -0,0 +1,3 @@+taking place during, the framework of [1m[91manother[0m work of fiction by the same or+[1m[91manother[0m author. Parallel novels or "reimagined classics" are works of fiction+the characters of one book with those of [1m[91manother[0m".
+ test/golden/test-files/dir1/file3 view
@@ -0,0 +1,9 @@+Brandon is a small town and civil parish in the English county of Suffolk. It+is in the Forest Heath local government district. Brandon is located in the+Breckland area on the border of Suffolk with the adjoining county of Norfolk.+Surrounded by Forestry Commission and agricultural land it is considered a+rural town. According to Eilert Ekwall (The Concise Oxford Dictionary of+English Place Names) the likely origin of the name is "Brandon, usually 'hill+where broom grows'", the earliest known spelling being in the 11th century when+the town, gradually expanding up and along the rising ground of the river+valley, was called Bromdun.
+ test/golden/test-files/dir1/file4 view
@@ -0,0 +1,9 @@+William Nevins Armstrong (March 10, 1835 – October 16, 1905), aka Nevins+Armstrong and aka W. N. Armstrong, was the Attorney General of Hawaii during+the reign of King David Kalākaua. He is most widely known outside of Hawaii for+the book Around the World with a King, his insider account of King Kalākaua's+world tour in 1881. He was born in Lahaina on the island of Maui, the third of+ten children of missionaries Clarissa Chapman Armstrong and Richard Armstrong,+who later served as the second kahu (pastor) of Kawaiahaʻo Church, and+subsequently was appointed President of the Board of Education for the Kingdom+of Hawaii.
+ test/golden/test-files/dir1/subdir1/file5 view
@@ -0,0 +1,6 @@+Laura Belém is a Brazilian artist and lecturer. Her work has been exhibited in+Brazil, Denmark, Canada, France, Japan, Italy and the United Kingdom. Born+1974, Belo Horizonte (Brazil), Belém graduated with an MA degree in Fine Art at+Central Saint Martins College of Art and Design in London, UK in 2000. She has+been awarded grants by bodies such as The Triangle Association in Brooklyn and+The Casa de Velázquez in Madrid.
+ test/golden/test-files/dir2/file6 view
@@ -0,0 +1,5 @@+Boyde may refer to: Danish orthography is the system used to write the Danish+language. The oldest preserved examples of written Danish are in the Runic+alphabet, but by the end of the High Middle Ages the Runes had mostly been+replaced by the Latin letters. Danish currently uses a 29-letter Latin-script+alphabet, identical to the Norwegian alphabet.
+ test/golden/test-files/empty-file view
+ test/golden/test-files/file1 view
@@ -0,0 +1,5 @@+The parallel novel is a piece of literature written within, derived from, or+taking place during, the framework of another work of fiction by the same or+another author. Parallel novels or "reimagined classics" are works of fiction+that "borrow a character and fill in his story, mirror an 'old' plot, or blend+the characters of one book with those of another".
+ test/golden/test-files/file2 view
@@ -0,0 +1,7 @@+Proud Pour is a wine company that funds solutions to local environmental+problems. Their first product, a Sauvignon Blanc called "The Oyster", funds the+restoration of 100 wild oysters per bottle to New York Harbor and coastal+Massachusetts. Their first restoration partners are the Billion Oyster Project+and the Massachusetts Oyster Project. The company was founded by Berlin Crystal+Kelly in 2014 in New York City, where she was actively involved with the New+York City Homebrewers Guild.
+ test/golden/test-files/from-grep view
@@ -0,0 +1,14 @@+test/golden/test-files/dir1/file3:Brandon is a small town and civil parish in the English county of Suffolk. It+test/golden/test-files/dir1/file3:is in the Forest Heath local government district. Brandon is located in the+test/golden/test-files/dir1/file3:Breckland area on the border of Suffolk with the adjoining county of Norfolk.+test/golden/test-files/dir1/file3:Surrounded by Forestry Commission and agricultural land it is considered a+test/golden/test-files/dir1/file3:English Place Names) the likely origin of the name is "Brandon, usually 'hill+test/golden/test-files/dir1/file3:the town, gradually expanding up and along the rising ground of the river+test/golden/test-files/dir1/subdir1/file5:Laura Belém is a Brazilian artist and lecturer. Her work has been exhibited in+test/golden/test-files/dir1/subdir1/file5:Brazil, Denmark, Canada, France, Japan, Italy and the United Kingdom. Born+test/golden/test-files/dir1/subdir1/file5:Central Saint Martins College of Art and Design in London, UK in 2000. She has+test/golden/test-files/dir1/subdir1/file5:been awarded grants by bodies such as The Triangle Association in Brooklyn and+test/golden/test-files/dir1/file4:Armstrong and aka W. N. Armstrong, was the Attorney General of Hawaii during+test/golden/test-files/dir1/file4:world tour in 1881. He was born in Lahaina on the island of Maui, the third of+test/golden/test-files/dir1/file4:ten children of missionaries Clarissa Chapman Armstrong and Richard Armstrong,+test/golden/test-files/dir1/file4:who later served as the second kahu (pastor) of Kawaiahaʻo Church, and