packages feed

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 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+=========++[![Build Status](https://secure.travis-ci.org/cdepillabout/highlight.svg)](http://travis-ci.org/cdepillabout/highlight)+[![Hackage](https://img.shields.io/hackage/v/highlight.svg)](https://hackage.haskell.org/package/highlight)+[![Stackage LTS](http://stackage.org/package/highlight/badge/lts)](http://stackage.org/lts/package/highlight)+[![Stackage Nightly](http://stackage.org/package/highlight/badge/nightly)](http://stackage.org/nightly/package/highlight)+![BSD3 license](https://img.shields.io/badge/license-BSD3-blue.svg)++`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:++![example screenshot](/img/pretty-simple-example-screenshot.png?raw=true "example screenshot")++## 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