diff --git a/README.markdown b/README.markdown
--- a/README.markdown
+++ b/README.markdown
@@ -74,6 +74,8 @@
 
 &nbsp;&nbsp;&nbsp;&nbsp;&#x2612;&nbsp;&nbsp;2017-03-31  v0.12.0.0 [Move IsRegex into Text.RE](https://github.com/iconnect/regex/milestone/16)
 
+&nbsp;&nbsp;&nbsp;&nbsp;&#x2612;&nbsp;&nbsp;2017-04-02  v0.13.0.0 [Protect findCaptureID and add Find, re-sort-imports, tutorials](https://github.com/iconnect/regex/milestone/17)
+
 &nbsp;&nbsp;&nbsp;&nbsp;&#x2610;&nbsp;&nbsp;2017-04-03  v1.0.0.0  [First stable release](https://github.com/iconnect/regex/milestone/3)
 
 &nbsp;&nbsp;&nbsp;&nbsp;&#x2610;&nbsp;&nbsp;2017-08-31  v2.0.0.0  [Fast text replacement with benchmarks](https://github.com/iconnect/regex/milestone/4)
diff --git a/Text/RE/REOptions.lhs b/Text/RE/REOptions.lhs
--- a/Text/RE/REOptions.lhs
+++ b/Text/RE/REOptions.lhs
@@ -23,8 +23,8 @@
   , emptyMacros
   ) where
 
-import           Data.Hashable
 import qualified Data.HashMap.Strict        as HM
+import           Data.Hashable
 import           Data.String
 import           Language.Haskell.TH
 import           Language.Haskell.TH.Syntax
diff --git a/Text/RE/TDFA/ByteString.hs b/Text/RE/TDFA/ByteString.hs
--- a/Text/RE/TDFA/ByteString.hs
+++ b/Text/RE/TDFA/ByteString.hs
@@ -59,9 +59,9 @@
   , module Text.RE.ZeInternals.SearchReplace.TDFA.ByteString
   ) where
 
-import           Prelude.Compat
 import qualified Data.ByteString               as B
 import           Data.Typeable
+import           Prelude.Compat
 import           Text.RE.REOptions
 import           Text.RE.Replace
 import           Text.RE.ZeInternals.AddCaptureNames
diff --git a/Text/RE/TDFA/ByteString/Lazy.hs b/Text/RE/TDFA/ByteString/Lazy.hs
--- a/Text/RE/TDFA/ByteString/Lazy.hs
+++ b/Text/RE/TDFA/ByteString/Lazy.hs
@@ -59,9 +59,9 @@
   , module Text.RE.ZeInternals.SearchReplace.TDFA.ByteString.Lazy
   ) where
 
-import           Prelude.Compat
 import qualified Data.ByteString.Lazy.Char8    as LBS
 import           Data.Typeable
+import           Prelude.Compat
 import           Text.RE.REOptions
 import           Text.RE.Replace
 import           Text.RE.ZeInternals.AddCaptureNames
diff --git a/Text/RE/TDFA/Sequence.hs b/Text/RE/TDFA/Sequence.hs
--- a/Text/RE/TDFA/Sequence.hs
+++ b/Text/RE/TDFA/Sequence.hs
@@ -59,9 +59,9 @@
   , module Text.RE.ZeInternals.SearchReplace.TDFA.Sequence
   ) where
 
-import           Prelude.Compat
 import qualified Data.Sequence                 as S
 import           Data.Typeable
+import           Prelude.Compat
 import           Text.RE.REOptions
 import           Text.RE.Replace
 import           Text.RE.ZeInternals.AddCaptureNames
diff --git a/Text/RE/TDFA/String.hs b/Text/RE/TDFA/String.hs
--- a/Text/RE/TDFA/String.hs
+++ b/Text/RE/TDFA/String.hs
@@ -59,9 +59,9 @@
   , module Text.RE.ZeInternals.SearchReplace.TDFA.String
   ) where
 
-import           Prelude.Compat
 
 import           Data.Typeable
+import           Prelude.Compat
 import           Text.RE.REOptions
 import           Text.RE.Replace
 import           Text.RE.ZeInternals.AddCaptureNames
diff --git a/Text/RE/TDFA/Text.hs b/Text/RE/TDFA/Text.hs
--- a/Text/RE/TDFA/Text.hs
+++ b/Text/RE/TDFA/Text.hs
@@ -59,9 +59,9 @@
   , module Text.RE.ZeInternals.SearchReplace.TDFA.Text
   ) where
 
-import           Prelude.Compat
 import qualified Data.Text                     as T
 import           Data.Typeable
+import           Prelude.Compat
 import           Text.RE.REOptions
 import           Text.RE.Replace
 import           Text.RE.ZeInternals.AddCaptureNames
diff --git a/Text/RE/TDFA/Text/Lazy.hs b/Text/RE/TDFA/Text/Lazy.hs
--- a/Text/RE/TDFA/Text/Lazy.hs
+++ b/Text/RE/TDFA/Text/Lazy.hs
@@ -59,9 +59,9 @@
   , module Text.RE.ZeInternals.SearchReplace.TDFA.Text.Lazy
   ) where
 
-import           Prelude.Compat
 import qualified Data.Text.Lazy                as TL
 import           Data.Typeable
+import           Prelude.Compat
 import           Text.RE.REOptions
 import           Text.RE.Replace
 import           Text.RE.ZeInternals.AddCaptureNames
diff --git a/Text/RE/TestBench.hs b/Text/RE/TestBench.hs
--- a/Text/RE/TestBench.hs
+++ b/Text/RE/TestBench.hs
@@ -43,147 +43,10 @@
   , Severity(..)
   , parseSeverity
   , severityKeywords
+  -- * Text.RE
+  , module Text.RE
   ) where
 
-import           Data.Array
-import qualified Data.HashMap.Strict        as HM
-import           Data.Maybe
-import qualified Data.Text                  as T
-import           Data.Time
-import qualified Data.Time.Locale.Compat    as LC
-import           Data.Word
-import           Text.Printf
-import           Text.Read
-import           Text.RE.Replace
+import           Text.RE
 import           Text.RE.ZeInternals.TestBench
-
-
-parseInteger :: Replace a => a -> Maybe Int
-parseInteger = readMaybe . unpackR
-
-parseHex :: Replace a => a -> Maybe Int
-parseHex = readMaybe . ("0x"++) . unpackR
-
-parseDouble :: Replace a => a -> Maybe Double
-parseDouble = readMaybe . unpackR
-
-parseString :: Replace a => a -> Maybe T.Text
-parseString = readMaybe . unpackR
-
-parseSimpleString :: Replace a => a -> Maybe T.Text
-parseSimpleString = Just . T.dropEnd 1 . T.drop 1 . textifyR
-
-date_templates, time_templates, timezone_templates,
-  date_time_8601_templates, date_time_templates :: [String]
-date_templates            = ["%F"]
-time_templates            = ["%H:%M:%S","%H:%M:%S%Q","%H:%M"]
-timezone_templates        = ["Z","%z"]
-date_time_8601_templates  =
-    [ printf "%sT%s%s" dt tm tz
-        | dt <- date_templates
-        , tm <- time_templates
-        , tz <- timezone_templates
-        ]
-date_time_templates       =
-    [ printf "%s%c%s%s" dt sc tm tz
-        | dt <- date_templates
-        , sc <- ['T',' ']
-        , tm <- time_templates
-        , tz <- timezone_templates ++ [" UTC",""]
-        ]
-
-parseDate :: Replace a => a -> Maybe Day
-parseDate = parse_time date_templates
-
-parseSlashesDate :: Replace a => a -> Maybe Day
-parseSlashesDate = parse_time ["%Y/%m/%d"]
-
-parseTimeOfDay :: Replace a => a -> Maybe TimeOfDay
-parseTimeOfDay = parse_time time_templates
-
-parseTimeZone :: Replace a => a -> Maybe TimeZone
-parseTimeZone = parse_time timezone_templates
-
-parseDateTime :: Replace a => a -> Maybe UTCTime
-parseDateTime = parse_time date_time_templates
-
-parseDateTime8601 :: Replace a => a -> Maybe UTCTime
-parseDateTime8601 = parse_time date_time_8601_templates
-
-parseDateTimeCLF :: Replace a => a -> Maybe UTCTime
-parseDateTimeCLF = parse_time ["%d/%b/%Y:%H:%M:%S %z"]
-
-parseShortMonth :: Replace a => a -> Maybe Int
-parseShortMonth = flip HM.lookup short_month_hm . unpackR
-
-parse_time :: (ParseTime t,Replace s) => [String] -> s -> Maybe t
-parse_time tpls = prs . unpackR
-  where
-    prs s = listToMaybe $ catMaybes
-      [ parseTime LC.defaultTimeLocale fmt s
-          | fmt<-tpls
-          ]
-
-short_month_hm :: HM.HashMap String Int
-short_month_hm = HM.fromList [ (T.unpack $ shortMonthArray!i,i) | i<-[1..12] ]
-
-shortMonthArray :: Array Int T.Text
-shortMonthArray = listArray (1,12) $
-  T.words "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
-
-type IPV4Address = (Word8,Word8,Word8,Word8)
-
-parseIPv4Address :: Replace a => a -> Maybe IPV4Address
-parseIPv4Address = prs . words_by (=='.') . unpackR
-  where
-    prs [a_s,b_s,c_s,d_s] = do
-      a <- readMaybe a_s
-      b <- readMaybe b_s
-      c <- readMaybe c_s
-      d <- readMaybe d_s
-      case all is_o [a,b,c,d] of
-        True  -> Just (toEnum a,toEnum b,toEnum c,toEnum d)
-        False -> Nothing
-    prs _ = Nothing
-
-    is_o x = 0 <= x && x <= 255
-
-data Severity
-  = Emerg
-  | Alert
-  | Crit
-  | Err
-  | Warning
-  | Notice
-  | Info
-  | Debug
-  deriving (Bounded,Enum,Ord,Eq,Show)
-
-parseSeverity :: Replace a => a -> Maybe Severity
-parseSeverity = flip HM.lookup severity_hm . textifyR
-
-severity_hm :: HM.HashMap T.Text Severity
-severity_hm = HM.fromList
-  [ (kw,pri)
-      | pri<-[minBound..maxBound]
-      , let (kw0,kws) = severityKeywords pri
-      , kw <- kw0:kws
-      ]
-
-severityKeywords :: Severity -> (T.Text,[T.Text])
-severityKeywords pri = case pri of
-  Emerg     -> (,) "emerg"    ["panic"]
-  Alert     -> (,) "alert"    []
-  Crit      -> (,) "crit"     []
-  Err       -> (,) "err"      ["error"]
-  Warning   -> (,) "warning"  ["warn"]
-  Notice    -> (,) "notice"   []
-  Info      -> (,) "info"     []
-  Debug     -> (,) "debug"    []
-
-words_by :: (Char->Bool) -> String -> [String]
-words_by f s = case dropWhile f s of
-  "" -> []
-  s' -> w : words_by f s''
-        where
-          (w, s'') = break f s'
+import           Text.RE.ZeInternals.TestBench.Parsers
diff --git a/Text/RE/Tools.hs b/Text/RE/Tools.hs
--- a/Text/RE/Tools.hs
+++ b/Text/RE/Tools.hs
@@ -8,13 +8,21 @@
   , sed'
   -- * Grep
   , grep
+  , Verbosity(..)
+  , Line(..)
   , grepLines
+  , grepFilter
   , GrepScript
-  , grepScript
+  , grepWithScript
+  , report
   , linesMatched
   -- * Lex
   , alex
   , alex'
+  -- * Find
+  , FindMethods(..)
+  , findMatches
+  , findMatches'
   -- * IsRegex
   , IsRegex(..)
   -- * Edit
@@ -35,6 +43,7 @@
 
 import           Text.RE
 import           Text.RE.Tools.Edit
+import           Text.RE.Tools.Find
 import           Text.RE.Tools.Grep
 import           Text.RE.Tools.Lex
 import           Text.RE.Tools.Sed
diff --git a/Text/RE/Tools/Find.lhs b/Text/RE/Tools/Find.lhs
new file mode 100644
--- /dev/null
+++ b/Text/RE/Tools/Find.lhs
@@ -0,0 +1,71 @@
+\begin{code}
+{-# LANGUAGE NoImplicitPrelude          #-}
+{-# LANGUAGE RecordWildCards            #-}
+{-# LANGUAGE CPP                        #-}
+#if __GLASGOW_HASKELL__ >= 800
+{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
+#endif
+
+module Text.RE.Tools.Find
+  ( FindMethods(..)
+  , findMatches
+  , findMatches'
+  -- * Text.RE
+  , module Text.RE
+  ) where
+
+import qualified Data.List                      as L
+import           Prelude.Compat
+import           Text.RE
+import           Text.RE.Replace
+
+
+-- | as we don't want the @directory@ and FilePath dependencies
+-- we will abstract the three calls we need into this record type
+data FindMethods s =
+  FindMethods
+    { doesDirectoryExistDM :: s -> IO Bool    -- ^ doesDirectoryExist from
+                                              -- System.Directory
+    , listDirectoryDM      :: s -> IO [s]     -- ^ either getDirectoryContents
+                                              -- or listDirectory from
+                                              -- System.Directory
+    , combineDM            :: s -> s -> s     -- ^ </> from System.FilePath
+    }
+
+
+-- | recursively list all files whose filename matches given RE,
+-- sorting the list into ascending order;
+-- if the argument path has a trailing '/' then it will be removed
+findMatches :: IsRegex re s => FindMethods s -> re -> s -> IO [s]
+findMatches fm = findMatches' fm L.sort matched
+
+-- | recursively list all files whose filename matches given RE,
+-- using the given function to determine which matches to accept;
+findMatches' :: IsRegex re s
+             => FindMethods s         -- ^ the directory and filepath methods
+             -> ([s]->[s])            -- ^ result post-processing function
+             -> (Match s->Bool)       -- ^ filtering function
+             -> re                    -- ^ re to be matched against the leaf filename
+             -> s                     -- ^ root directory of the search
+             -> IO [s]
+findMatches' fm srt tst re fp = srt <$> find_ fm tst re (packR "") fp
+
+find_ :: IsRegex re s
+      => FindMethods s
+      -> (Match s->Bool)
+      -> re
+      -> s
+      -> s
+      -> IO [s]
+find_ fm@FindMethods{..} tst re fn fp = do
+  is_dir <- doesDirectoryExistDM fp
+  case is_dir of
+    True  -> do
+      fns <- filter ordinary <$> listDirectoryDM fp
+      concat <$>
+        mapM (uncurry $ find_ fm tst re) [ (fn_,abs_path fn_) | fn_<-fns ]
+    False -> return [ fp | lengthR fp /= 0 && tst (matchOnce re fn) ]
+  where
+    abs_path fn_ = fp `combineDM` fn_
+    ordinary fn_ = not $ fn_ `elem` [packR ".",packR ".."]
+\end{code}
diff --git a/Text/RE/Tools/Grep.lhs b/Text/RE/Tools/Grep.lhs
--- a/Text/RE/Tools/Grep.lhs
+++ b/Text/RE/Tools/Grep.lhs
@@ -6,12 +6,13 @@
 
 module Text.RE.Tools.Grep
   ( grep
+  , Verbosity(..)
   , Line(..)
   , grepLines
+  , grepFilter
   , GrepScript
-  , grepScript
+  , grepWithScript
   , report
-  , Verbosity(..)
   , linesMatched
   -- * IsRegex
   , IsRegex(..)
@@ -28,6 +29,7 @@
 import           Prelude.Compat
 import           Text.Printf
 import           Text.RE
+import           Text.RE.ZeInternals.Replace
 import           Text.RE.ZeInternals.Types.LineNo
 
 
@@ -35,20 +37,33 @@
 grep :: IsRegex re LBS.ByteString => Verbosity -> re -> FilePath -> IO ()
 grep v rex fp = grepLines rex fp >>= putStr . report v
 
+-- | specifies whether to return the linss matched or missed
+data Verbosity
+  = LinesMatched
+  | LinesNotMatched
+  deriving (Show,Eq,Ord)
+
 -- | 'grepLines' returns a 'Line' for each line in the file, listing all
 -- of the 'Matches' for that line
-data Line =
+data Line s =
   Line
-    { getLineNumber  :: LineNo                  -- ^ the 'LineNo' for this line
-    , getLineMatches :: Matches LBS.ByteString  -- ^ all the 'Matches' of the RE on this line
+    { getLineNumber  :: LineNo    -- ^ the 'LineNo' for this line
+    , getLineMatches :: Matches s -- ^ all the 'Matches' of the RE on this line
     }
   deriving (Show)
 
--- | returns a 'Line' for each line in the file enumerating all of the
--- matches for that line.
-grepLines :: IsRegex re LBS.ByteString => re -> FilePath -> IO [Line]
-grepLines rex fp =
-    grepScript [(rex,mk)] . LBS.lines <$> LBS.readFile fp
+-- | returns a 'Line' for each line in the file, enumerating all of the
+-- matches for that line
+grepLines :: IsRegex re LBS.ByteString
+          => re
+          -> FilePath
+          -> IO [Line LBS.ByteString]
+grepLines rex fp = grepFilter rex <$> LBS.readFile fp
+
+-- | returns a 'Line' for each line in the argument text, enumerating
+-- all of the matches for that line
+grepFilter :: IsRegex re s => re -> s -> [Line s]
+grepFilter rex = grepWithScript [(rex,mk)] . linesR
   where
     mk i mtchs = Just $ Line i mtchs
 
@@ -57,8 +72,8 @@
 type GrepScript re s t = [(re,LineNo -> Matches s -> Maybe t)]
 
 -- | given a list of lines, apply the 'GrepScript' to each line of the file
-grepScript :: IsRegex re s => GrepScript re s t -> [s] -> [t]
-grepScript scr = loop firstLine
+grepWithScript :: IsRegex re s => GrepScript re s t -> [s] -> [t]
+grepWithScript scr = loop firstLine
   where
     loop _ []       = []
     loop i (ln:lns) = seq i $ choose i ln lns scr
@@ -69,22 +84,16 @@
       Just t  -> t : loop (succ i) lns
 
 -- | generate a grep report from a list of 'Line'
-report :: Verbosity -> [Line] -> String
+report :: Verbosity -> [Line LBS.ByteString] -> String
 report v = unlines . map fmt . linesMatched v
   where
     fmt Line{..} =
       printf "%05d %s" (getLineNo getLineNumber) $
           LBS.unpack $ matchesSource getLineMatches
 
--- | specifies whether to return the linss matched or missed
-data Verbosity
-  = LinesMatched
-  | LinesNotMatched
-  deriving (Show,Eq,Ord)
-
 -- | given a 'velocity' flag filter out either the lines matched or not
 -- matched
-linesMatched :: Verbosity -> [Line] -> [Line]
+linesMatched :: Verbosity -> [Line s] -> [Line s]
 linesMatched v = filter $ f . anyMatches . getLineMatches
   where
     f = case v of
diff --git a/Text/RE/Tools/Lex.hs b/Text/RE/Tools/Lex.hs
--- a/Text/RE/Tools/Lex.hs
+++ b/Text/RE/Tools/Lex.hs
@@ -8,4 +8,4 @@
   ) where
 
 import           Text.RE
-import           Text.RE.ZeInternals.Lex
+import           Text.RE.ZeInternals.Tools.Lex
diff --git a/Text/RE/Tools/Sed.lhs b/Text/RE/Tools/Sed.lhs
--- a/Text/RE/Tools/Sed.lhs
+++ b/Text/RE/Tools/Sed.lhs
@@ -56,10 +56,10 @@
      => Edits m re a
      -> a
      -> m a
-sed' as lbs = do
+sed' as t = do
   mconcat <$> sequence
     [ applyEdits lno as s
-        | (lno,s)<-zip [firstLine..] $ linesR lbs
+        | (lno,s)<-zip [firstLine..] $ linesR t
         ]
 
 read_file :: FilePath -> IO LBS.ByteString
diff --git a/Text/RE/ZeInternals/AddCaptureNames.hs b/Text/RE/ZeInternals/AddCaptureNames.hs
--- a/Text/RE/ZeInternals/AddCaptureNames.hs
+++ b/Text/RE/ZeInternals/AddCaptureNames.hs
@@ -6,8 +6,8 @@
 
 module Text.RE.ZeInternals.AddCaptureNames where
 
-import qualified Data.ByteString.Lazy.Char8    as LBS
 import qualified Data.ByteString.Char8         as B
+import qualified Data.ByteString.Lazy.Char8    as LBS
 import           Data.Dynamic
 import           Data.Maybe
 import qualified Data.Sequence                 as S
diff --git a/Text/RE/ZeInternals/Lex.lhs b/Text/RE/ZeInternals/Lex.lhs
deleted file mode 100644
--- a/Text/RE/ZeInternals/Lex.lhs
+++ /dev/null
@@ -1,45 +0,0 @@
-\begin{code}
-{-# LANGUAGE NoImplicitPrelude          #-}
-
-module Text.RE.ZeInternals.Lex
-  ( alex
-  , alex'
-  ) where
-
-import           Prelude.Compat
-import           Text.RE.Replace
-import           Text.RE.ZeInternals.Types.IsRegex
-
-
--- | a simple regex-based scanner interpretter for prototyping
--- scanners
-alex :: IsRegex re s => [(re,Match s->Maybe t)] -> t -> s -> [t]
-alex = alex' matchOnce
-
--- | a higher order version of 'alex' parameterised over the @matchOnce@
--- function
-alex' :: Replace s
-      => (re->s->Match s)
-      -> [(re,Match s->Maybe t)]
-      -> t
-      -> s
-      -> [t]
-alex' mo al t_err = loop
-  where
-    loop s = case lengthR s == 0 of
-      True  -> []
-      False -> choose al s
-
-    choose []           _ = [t_err]
-    choose ((re,f):al') s = case mb_p of
-        Just (s',t) -> t : loop s'
-        _           -> choose al' s
-      where
-        mb_p = do
-          cap <- matchCapture mtch
-          case captureOffset cap == 0 of
-            True  -> (,) (captureSuffix cap) <$> f mtch
-            False -> Nothing
-
-        mtch = mo re s
-\end{code}
diff --git a/Text/RE/ZeInternals/NamedCaptures.lhs b/Text/RE/ZeInternals/NamedCaptures.lhs
--- a/Text/RE/ZeInternals/NamedCaptures.lhs
+++ b/Text/RE/ZeInternals/NamedCaptures.lhs
@@ -27,10 +27,10 @@
 import           GHC.Generics
 import qualified Language.Haskell.TH          as TH
 import           Language.Haskell.TH.Quote
-import           Text.RE.ZeInternals.Lex
 import           Text.RE.ZeInternals.PreludeMacros
 import           Text.RE.ZeInternals.QQ
 import           Text.RE.ZeInternals.TestBench
+import           Text.RE.ZeInternals.Tools.Lex
 import           Text.RE.ZeInternals.Types.CaptureID
 import           Text.RE.ZeInternals.Types.Match
 import           Text.Regex.TDFA
diff --git a/Text/RE/ZeInternals/PreludeMacros.hs b/Text/RE/ZeInternals/PreludeMacros.hs
--- a/Text/RE/ZeInternals/PreludeMacros.hs
+++ b/Text/RE/ZeInternals/PreludeMacros.hs
@@ -30,9 +30,9 @@
 import qualified Data.Text                    as T
 import           Data.Time
 import           Prelude.Compat
-import           Text.RE.TestBench
-import           Text.RE.ZeInternals.TestBench
 import           Text.RE.REOptions
+import           Text.RE.ZeInternals.TestBench
+import           Text.RE.ZeInternals.TestBench.Parsers
 
 
 -- | generate the standard prelude Macros used to parse REs
diff --git a/Text/RE/ZeInternals/Replace.lhs b/Text/RE/ZeInternals/Replace.lhs
--- a/Text/RE/ZeInternals/Replace.lhs
+++ b/Text/RE/ZeInternals/Replace.lhs
@@ -48,11 +48,11 @@
 import qualified Data.Text.Encoding             as TE
 import qualified Data.Text.Lazy                 as LT
 import           Prelude.Compat
+import           Text.RE.REOptions
 import           Text.RE.ZeInternals.Types.Capture
 import           Text.RE.ZeInternals.Types.CaptureID
 import           Text.RE.ZeInternals.Types.Match
 import           Text.RE.ZeInternals.Types.Matches
-import           Text.RE.REOptions
 import           Text.Read
 import           Text.Regex.TDFA
 import           Text.Regex.TDFA.Text()
diff --git a/Text/RE/ZeInternals/SearchReplace/TDFA.hs b/Text/RE/ZeInternals/SearchReplace/TDFA.hs
--- a/Text/RE/ZeInternals/SearchReplace/TDFA.hs
+++ b/Text/RE/ZeInternals/SearchReplace/TDFA.hs
@@ -23,8 +23,8 @@
 import           Language.Haskell.TH
 import           Language.Haskell.TH.Quote
 import           Prelude.Compat
-import           Text.RE.ZeInternals.SearchReplace.TDFAEdPrime
 import           Text.RE.REOptions
+import           Text.RE.ZeInternals.SearchReplace.TDFAEdPrime
 
 
 -- | the @[ed| ... /// ... |]@ quasi quoters
diff --git a/Text/RE/ZeInternals/SearchReplace/TDFA/ByteString.hs b/Text/RE/ZeInternals/SearchReplace/TDFA/ByteString.hs
--- a/Text/RE/ZeInternals/SearchReplace/TDFA/ByteString.hs
+++ b/Text/RE/ZeInternals/SearchReplace/TDFA/ByteString.hs
@@ -24,9 +24,9 @@
 import qualified Data.ByteString.Char8         as B
 import           Language.Haskell.TH
 import           Language.Haskell.TH.Quote
-import           Text.RE.ZeInternals.TDFA
-import           Text.RE.ZeInternals.SearchReplace.TDFAEdPrime
 import           Text.RE.REOptions
+import           Text.RE.ZeInternals.SearchReplace.TDFAEdPrime
+import           Text.RE.ZeInternals.TDFA
 import           Text.RE.ZeInternals.Types.SearchReplace
 
 
diff --git a/Text/RE/ZeInternals/SearchReplace/TDFA/ByteString/Lazy.hs b/Text/RE/ZeInternals/SearchReplace/TDFA/ByteString/Lazy.hs
--- a/Text/RE/ZeInternals/SearchReplace/TDFA/ByteString/Lazy.hs
+++ b/Text/RE/ZeInternals/SearchReplace/TDFA/ByteString/Lazy.hs
@@ -24,9 +24,9 @@
 import qualified Data.ByteString.Lazy.Char8    as LBS
 import           Language.Haskell.TH
 import           Language.Haskell.TH.Quote
-import           Text.RE.ZeInternals.TDFA
-import           Text.RE.ZeInternals.SearchReplace.TDFAEdPrime
 import           Text.RE.REOptions
+import           Text.RE.ZeInternals.SearchReplace.TDFAEdPrime
+import           Text.RE.ZeInternals.TDFA
 import           Text.RE.ZeInternals.Types.SearchReplace
 
 
diff --git a/Text/RE/ZeInternals/SearchReplace/TDFA/Sequence.hs b/Text/RE/ZeInternals/SearchReplace/TDFA/Sequence.hs
--- a/Text/RE/ZeInternals/SearchReplace/TDFA/Sequence.hs
+++ b/Text/RE/ZeInternals/SearchReplace/TDFA/Sequence.hs
@@ -24,9 +24,9 @@
 import qualified Data.Sequence                 as S
 import           Language.Haskell.TH
 import           Language.Haskell.TH.Quote
-import           Text.RE.ZeInternals.TDFA
-import           Text.RE.ZeInternals.SearchReplace.TDFAEdPrime
 import           Text.RE.REOptions
+import           Text.RE.ZeInternals.SearchReplace.TDFAEdPrime
+import           Text.RE.ZeInternals.TDFA
 import           Text.RE.ZeInternals.Types.SearchReplace
 
 
diff --git a/Text/RE/ZeInternals/SearchReplace/TDFA/String.hs b/Text/RE/ZeInternals/SearchReplace/TDFA/String.hs
--- a/Text/RE/ZeInternals/SearchReplace/TDFA/String.hs
+++ b/Text/RE/ZeInternals/SearchReplace/TDFA/String.hs
@@ -24,9 +24,9 @@
 
 import           Language.Haskell.TH
 import           Language.Haskell.TH.Quote
-import           Text.RE.ZeInternals.TDFA
-import           Text.RE.ZeInternals.SearchReplace.TDFAEdPrime
 import           Text.RE.REOptions
+import           Text.RE.ZeInternals.SearchReplace.TDFAEdPrime
+import           Text.RE.ZeInternals.TDFA
 import           Text.RE.ZeInternals.Types.SearchReplace
 
 
diff --git a/Text/RE/ZeInternals/SearchReplace/TDFA/Text.hs b/Text/RE/ZeInternals/SearchReplace/TDFA/Text.hs
--- a/Text/RE/ZeInternals/SearchReplace/TDFA/Text.hs
+++ b/Text/RE/ZeInternals/SearchReplace/TDFA/Text.hs
@@ -24,9 +24,9 @@
 import qualified Data.Text                     as T
 import           Language.Haskell.TH
 import           Language.Haskell.TH.Quote
-import           Text.RE.ZeInternals.TDFA
-import           Text.RE.ZeInternals.SearchReplace.TDFAEdPrime
 import           Text.RE.REOptions
+import           Text.RE.ZeInternals.SearchReplace.TDFAEdPrime
+import           Text.RE.ZeInternals.TDFA
 import           Text.RE.ZeInternals.Types.SearchReplace
 
 
diff --git a/Text/RE/ZeInternals/SearchReplace/TDFA/Text/Lazy.hs b/Text/RE/ZeInternals/SearchReplace/TDFA/Text/Lazy.hs
--- a/Text/RE/ZeInternals/SearchReplace/TDFA/Text/Lazy.hs
+++ b/Text/RE/ZeInternals/SearchReplace/TDFA/Text/Lazy.hs
@@ -24,9 +24,9 @@
 import qualified Data.Text.Lazy                as TL
 import           Language.Haskell.TH
 import           Language.Haskell.TH.Quote
-import           Text.RE.ZeInternals.TDFA
-import           Text.RE.ZeInternals.SearchReplace.TDFAEdPrime
 import           Text.RE.REOptions
+import           Text.RE.ZeInternals.SearchReplace.TDFAEdPrime
+import           Text.RE.ZeInternals.TDFA
 import           Text.RE.ZeInternals.Types.SearchReplace
 
 
diff --git a/Text/RE/ZeInternals/SearchReplace/TDFAEdPrime.hs b/Text/RE/ZeInternals/SearchReplace/TDFAEdPrime.hs
--- a/Text/RE/ZeInternals/SearchReplace/TDFAEdPrime.hs
+++ b/Text/RE/ZeInternals/SearchReplace/TDFAEdPrime.hs
@@ -18,8 +18,8 @@
 import           Prelude.Compat
 import           Text.RE.REOptions
 import           Text.RE.Replace
-import           Text.RE.ZeInternals.SearchReplace
 import           Text.RE.ZeInternals.QQ
+import           Text.RE.ZeInternals.SearchReplace
 import           Text.RE.ZeInternals.TDFA
 import           Text.RE.ZeInternals.Types.IsRegex
 import           Text.Regex.TDFA
diff --git a/Text/RE/ZeInternals/TestBench.lhs b/Text/RE/ZeInternals/TestBench.lhs
--- a/Text/RE/ZeInternals/TestBench.lhs
+++ b/Text/RE/ZeInternals/TestBench.lhs
@@ -41,8 +41,8 @@
 import           Data.Maybe
 import           Data.Ord
 import           Data.String
-import           Text.Printf
 import           Prelude.Compat
+import           Text.Printf
 import           Text.RE.REOptions
 import           Text.RE.ZeInternals.Replace
 import           Text.RE.ZeInternals.Types.Capture
diff --git a/Text/RE/ZeInternals/TestBench/Parsers.hs b/Text/RE/ZeInternals/TestBench/Parsers.hs
new file mode 100644
--- /dev/null
+++ b/Text/RE/ZeInternals/TestBench/Parsers.hs
@@ -0,0 +1,189 @@
+{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}
+{-# LANGUAGE OverloadedStrings                  #-}
+
+module Text.RE.ZeInternals.TestBench.Parsers
+  (
+  -- * The Test Bench
+    MacroEnv
+  , MacroDescriptor(..)
+  , RegexSource(..)
+  , WithCaptures(..)
+  , RegexType
+  , isTDFA
+  , isPCRE
+  , presentRegexType
+  -- ** Constructing a MacrosEnv
+  , mkMacros
+  -- ** Formatting Macros
+  , formatMacroTable
+  , formatMacroSummary
+  , formatMacroSources
+  , formatMacroSource
+  -- ** Formatting Macros
+  , testMacroEnv
+  , runTests
+  , runTests'
+  -- * Parsing
+  , parseInteger
+  , parseHex
+  , parseDouble
+  , parseString
+  , parseSimpleString
+  , parseDate
+  , parseSlashesDate
+  , parseTimeOfDay
+  , parseTimeZone
+  , parseDateTime
+  , parseDateTime8601
+  , parseDateTimeCLF
+  , parseShortMonth
+  , shortMonthArray
+  , IPV4Address
+  , parseIPv4Address
+  , Severity(..)
+  , parseSeverity
+  , severityKeywords
+  ) where
+
+import           Data.Array
+import qualified Data.HashMap.Strict        as HM
+import           Data.Maybe
+import qualified Data.Text                  as T
+import           Data.Time
+import qualified Data.Time.Locale.Compat    as LC
+import           Data.Word
+import           Text.Printf
+import           Text.RE.Replace
+import           Text.RE.ZeInternals.TestBench
+import           Text.Read
+
+
+parseInteger :: Replace a => a -> Maybe Int
+parseInteger = readMaybe . unpackR
+
+parseHex :: Replace a => a -> Maybe Int
+parseHex = readMaybe . ("0x"++) . unpackR
+
+parseDouble :: Replace a => a -> Maybe Double
+parseDouble = readMaybe . unpackR
+
+parseString :: Replace a => a -> Maybe T.Text
+parseString = readMaybe . unpackR
+
+parseSimpleString :: Replace a => a -> Maybe T.Text
+parseSimpleString = Just . T.dropEnd 1 . T.drop 1 . textifyR
+
+date_templates, time_templates, timezone_templates,
+  date_time_8601_templates, date_time_templates :: [String]
+date_templates            = ["%F"]
+time_templates            = ["%H:%M:%S","%H:%M:%S%Q","%H:%M"]
+timezone_templates        = ["Z","%z"]
+date_time_8601_templates  =
+    [ printf "%sT%s%s" dt tm tz
+        | dt <- date_templates
+        , tm <- time_templates
+        , tz <- timezone_templates
+        ]
+date_time_templates       =
+    [ printf "%s%c%s%s" dt sc tm tz
+        | dt <- date_templates
+        , sc <- ['T',' ']
+        , tm <- time_templates
+        , tz <- timezone_templates ++ [" UTC",""]
+        ]
+
+parseDate :: Replace a => a -> Maybe Day
+parseDate = parse_time date_templates
+
+parseSlashesDate :: Replace a => a -> Maybe Day
+parseSlashesDate = parse_time ["%Y/%m/%d"]
+
+parseTimeOfDay :: Replace a => a -> Maybe TimeOfDay
+parseTimeOfDay = parse_time time_templates
+
+parseTimeZone :: Replace a => a -> Maybe TimeZone
+parseTimeZone = parse_time timezone_templates
+
+parseDateTime :: Replace a => a -> Maybe UTCTime
+parseDateTime = parse_time date_time_templates
+
+parseDateTime8601 :: Replace a => a -> Maybe UTCTime
+parseDateTime8601 = parse_time date_time_8601_templates
+
+parseDateTimeCLF :: Replace a => a -> Maybe UTCTime
+parseDateTimeCLF = parse_time ["%d/%b/%Y:%H:%M:%S %z"]
+
+parseShortMonth :: Replace a => a -> Maybe Int
+parseShortMonth = flip HM.lookup short_month_hm . unpackR
+
+parse_time :: (ParseTime t,Replace s) => [String] -> s -> Maybe t
+parse_time tpls = prs . unpackR
+  where
+    prs s = listToMaybe $ catMaybes
+      [ parseTime LC.defaultTimeLocale fmt s
+          | fmt<-tpls
+          ]
+
+short_month_hm :: HM.HashMap String Int
+short_month_hm = HM.fromList [ (T.unpack $ shortMonthArray!i,i) | i<-[1..12] ]
+
+shortMonthArray :: Array Int T.Text
+shortMonthArray = listArray (1,12) $
+  T.words "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
+
+type IPV4Address = (Word8,Word8,Word8,Word8)
+
+parseIPv4Address :: Replace a => a -> Maybe IPV4Address
+parseIPv4Address = prs . words_by (=='.') . unpackR
+  where
+    prs [a_s,b_s,c_s,d_s] = do
+      a <- readMaybe a_s
+      b <- readMaybe b_s
+      c <- readMaybe c_s
+      d <- readMaybe d_s
+      case all is_o [a,b,c,d] of
+        True  -> Just (toEnum a,toEnum b,toEnum c,toEnum d)
+        False -> Nothing
+    prs _ = Nothing
+
+    is_o x = 0 <= x && x <= 255
+
+data Severity
+  = Emerg
+  | Alert
+  | Crit
+  | Err
+  | Warning
+  | Notice
+  | Info
+  | Debug
+  deriving (Bounded,Enum,Ord,Eq,Show)
+
+parseSeverity :: Replace a => a -> Maybe Severity
+parseSeverity = flip HM.lookup severity_hm . textifyR
+
+severity_hm :: HM.HashMap T.Text Severity
+severity_hm = HM.fromList
+  [ (kw,pri)
+      | pri<-[minBound..maxBound]
+      , let (kw0,kws) = severityKeywords pri
+      , kw <- kw0:kws
+      ]
+
+severityKeywords :: Severity -> (T.Text,[T.Text])
+severityKeywords pri = case pri of
+  Emerg     -> (,) "emerg"    ["panic"]
+  Alert     -> (,) "alert"    []
+  Crit      -> (,) "crit"     []
+  Err       -> (,) "err"      ["error"]
+  Warning   -> (,) "warning"  ["warn"]
+  Notice    -> (,) "notice"   []
+  Info      -> (,) "info"     []
+  Debug     -> (,) "debug"    []
+
+words_by :: (Char->Bool) -> String -> [String]
+words_by f s = case dropWhile f s of
+  "" -> []
+  s' -> w : words_by f s''
+        where
+          (w, s'') = break f s'
diff --git a/Text/RE/ZeInternals/Tools/Lex.lhs b/Text/RE/ZeInternals/Tools/Lex.lhs
new file mode 100644
--- /dev/null
+++ b/Text/RE/ZeInternals/Tools/Lex.lhs
@@ -0,0 +1,45 @@
+\begin{code}
+{-# LANGUAGE NoImplicitPrelude          #-}
+
+module Text.RE.ZeInternals.Tools.Lex
+  ( alex
+  , alex'
+  ) where
+
+import           Prelude.Compat
+import           Text.RE.Replace
+import           Text.RE.ZeInternals.Types.IsRegex
+
+
+-- | a simple regex-based scanner interpretter for prototyping
+-- scanners
+alex :: IsRegex re s => [(re,Match s->Maybe t)] -> t -> s -> [t]
+alex = alex' matchOnce
+
+-- | a higher order version of 'alex' parameterised over the @matchOnce@
+-- function
+alex' :: Replace s
+      => (re->s->Match s)
+      -> [(re,Match s->Maybe t)]
+      -> t
+      -> s
+      -> [t]
+alex' mo al t_err = loop
+  where
+    loop s = case lengthR s == 0 of
+      True  -> []
+      False -> choose al s
+
+    choose []           _ = [t_err]
+    choose ((re,f):al') s = case mb_p of
+        Just (s',t) -> t : loop s'
+        _           -> choose al' s
+      where
+        mb_p = do
+          cap <- matchCapture mtch
+          case captureOffset cap == 0 of
+            True  -> (,) (captureSuffix cap) <$> f mtch
+            False -> Nothing
+
+        mtch = mo re s
+\end{code}
diff --git a/Text/RE/ZeInternals/Types/CaptureID.hs b/Text/RE/ZeInternals/Types/CaptureID.hs
--- a/Text/RE/ZeInternals/Types/CaptureID.hs
+++ b/Text/RE/ZeInternals/Types/CaptureID.hs
@@ -2,10 +2,9 @@
 
 module Text.RE.ZeInternals.Types.CaptureID where
 
-import           Data.Ix
-import           Data.Hashable
 import qualified Data.HashMap.Strict            as HMS
-import           Data.Maybe
+import           Data.Hashable
+import           Data.Ix
 import qualified Data.Text                      as T
 
 
@@ -38,12 +37,16 @@
   deriving (Show,Ord,Eq,Enum,Ix,Num)
 
 -- | look up a 'CaptureID' in the 'CaptureNames' dictionary
-findCaptureID :: CaptureID -> CaptureNames -> Int
-findCaptureID (IsCaptureOrdinal o) _   = getCaptureOrdinal o
+unsafeFindCaptureID :: CaptureID -> CaptureNames -> Int
+unsafeFindCaptureID cid = either error id . findCaptureID cid
+
+-- | look up a 'CaptureID' in the 'CaptureNames' dictionary
+findCaptureID :: CaptureID -> CaptureNames -> Either String Int
+findCaptureID (IsCaptureOrdinal o) _   = Right $ getCaptureOrdinal o
 findCaptureID (IsCaptureName    n) hms =
-    getCaptureOrdinal $ fromMaybe oops $ HMS.lookup n hms
+    maybe oops (Right . getCaptureOrdinal) $ HMS.lookup n hms
   where
-    oops = error $ unlines $
+    oops = Left $ unlines $
       ("lookupCaptureID: " ++ T.unpack t ++ " not found in:") :
         [ "  "++T.unpack (getCaptureName nm) | nm <- HMS.keys hms ]
     t = getCaptureName n
diff --git a/Text/RE/ZeInternals/Types/Match.lhs b/Text/RE/ZeInternals/Types/Match.lhs
--- a/Text/RE/ZeInternals/Types/Match.lhs
+++ b/Text/RE/ZeInternals/Types/Match.lhs
@@ -31,9 +31,9 @@
 import           Data.Array
 import           Data.Maybe
 import           Data.Typeable
-import           Text.Regex.Base
 import           Text.RE.ZeInternals.Types.Capture
 import           Text.RE.ZeInternals.Types.CaptureID
+import           Text.Regex.Base
 
 infixl 9 !$, !$$
 \end{code}
@@ -142,17 +142,17 @@
 -- capture failed to capture anything (being in a failed alternate)
 captureMaybe :: CaptureID -> Match a -> Maybe (Capture a)
 captureMaybe cid mtch@Match{..} = do
+  i   <- lookupCaptureID cid mtch
   cap <- case bounds matchArray `inRange` CaptureOrdinal i of
     True  -> Just $ matchArray ! CaptureOrdinal i
     False -> Nothing
   case hasCaptured cap of
     True  -> Just cap
     False -> Nothing
-  where
-    i = lookupCaptureID cid mtch
 
-lookupCaptureID :: CaptureID -> Match a -> Int
-lookupCaptureID cid Match{..} = findCaptureID cid captureNames
+lookupCaptureID :: CaptureID -> Match a -> Maybe Int
+lookupCaptureID cid Match{..} =
+    either (const Nothing) Just $ findCaptureID cid captureNames
 \end{code}
 
 
diff --git a/Text/RE/ZeInternals/Types/Matches.lhs b/Text/RE/ZeInternals/Types/Matches.lhs
--- a/Text/RE/ZeInternals/Types/Matches.lhs
+++ b/Text/RE/ZeInternals/Types/Matches.lhs
@@ -18,10 +18,10 @@
 
 \begin{code}
 import           Data.Typeable
-import           Text.Regex.Base
 import           Text.RE.ZeInternals.Types.Capture
 import           Text.RE.ZeInternals.Types.CaptureID
 import           Text.RE.ZeInternals.Types.Match
+import           Text.Regex.Base
 \end{code}
 
 
diff --git a/changelog b/changelog
--- a/changelog
+++ b/changelog
@@ -1,5 +1,14 @@
 -*-change-log-*-
 
+0.13.0.0 Chris Dornan <chris.dornan@irisconnect.co.uk> 2017-04-03
+  * Add a Find Tool (#106)
+  * TestBench to export Text.RE (#107)
+  * Consolidate cabal templates (#108)
+  * Add special tutorials (#109)
+  * Add sort-imports example (#110)
+  * Generalise Grep (#111)
+  * Tighten up findCaptureID (#112)
+
 0.12.0.0 Chris Dornan <chris.dornan@irisconnect.co.uk> 2017-03-31
   * Add Text.RE.REOptions to RE.Summa (#103)
   * Move IsRegex into Text.RE (#104)
diff --git a/regex.cabal b/regex.cabal
--- a/regex.cabal
+++ b/regex.cabal
@@ -1,5 +1,5 @@
 Name:                   regex
-Version:                0.12.0.0
+Version:                0.13.0.0
 Synopsis:               Toolkit for regex-base
 Description:            A regular expression toolkit for regex-base with
                         compile-time checking of RE syntax, data types for
@@ -32,14 +32,12 @@
 Source-Repository this
     Type:               git
     Location:           https://github.com/iconnect/regex.git
-    Tag:                0.12.0.0
-
+    Tag:                0.13.0.0
 
 
 
 Library
     Hs-Source-Dirs:     .
-
     Exposed-Modules:
       Text.RE
       Text.RE.REOptions
@@ -55,13 +53,13 @@
       Text.RE.TestBench
       Text.RE.Tools
       Text.RE.Tools.Edit
+      Text.RE.Tools.Find
       Text.RE.Tools.Grep
       Text.RE.Tools.Lex
       Text.RE.Tools.Sed
       Text.RE.ZeInternals
       Text.RE.ZeInternals.AddCaptureNames
       Text.RE.ZeInternals.EscapeREString
-      Text.RE.ZeInternals.Lex
       Text.RE.ZeInternals.NamedCaptures
       Text.RE.ZeInternals.PreludeMacros
       Text.RE.ZeInternals.QQ
@@ -77,6 +75,8 @@
       Text.RE.ZeInternals.SearchReplace.TDFAEdPrime
       Text.RE.ZeInternals.TDFA
       Text.RE.ZeInternals.TestBench
+      Text.RE.ZeInternals.TestBench.Parsers
+      Text.RE.ZeInternals.Tools.Lex
       Text.RE.ZeInternals.Types.Capture
       Text.RE.ZeInternals.Types.CaptureID
       Text.RE.ZeInternals.Types.IsRegex
@@ -125,6 +125,7 @@
       , containers           >= 0.4
       , hashable             >= 1.2.3.3
       , regex-base           >= 0.93.2
+      , regex-pcre-builtin   >= 0.94.4.8.8.35
       , regex-tdfa           >= 1.2.0
       , regex-tdfa-text      >= 1.0.0.3
       , template-haskell     >= 2.7
