packages feed

screp-0.1.0.0: src/Scrappy/Grep/Config.hs

{-# LANGUAGE ScopedTypeVariables #-}

module Scrappy.Grep.Config
  ( runParserViaGhc
  , ConfigError(..)
  ) where

import System.Process (readProcessWithExitCode)
import System.Directory (doesFileExist, getTemporaryDirectory, removeFile)
import System.FilePath ((</>), takeDirectory, takeBaseName)
import System.Exit (ExitCode(..))
import Control.Exception (try, catch, SomeException)

data ConfigError
  = ConfigFileNotFound FilePath
  | GhcRunFailed String
  | ParseResultFailed String
  deriving (Show, Eq)

-- | Run a named parser from the import file via runghc
-- Returns the matches as a list of (line, col, matchText)
runParserViaGhc
  :: FilePath           -- ^ Import file path (Parsers.hs)
  -> String             -- ^ Parser name (e.g., "email")
  -> String             -- ^ Content to search
  -> IO (Either ConfigError [(Int, Int, String)])
runParserViaGhc importPath parserName content = do
  exists <- doesFileExist importPath
  if not exists
    then pure $ Left $ ConfigFileNotFound importPath
    else do
      tmpDir <- getTemporaryDirectory
      let runnerPath = tmpDir </> "PgrepRunner.hs"
          moduleName = takeBaseName importPath
          importDir = takeDirectory importPath

      -- Write the runner script
      writeFile runnerPath (generateRunner moduleName)

      -- Run it (need to expose parsec and containers packages via --ghc-arg)
      result <- try $ readProcessWithExitCode
        "runghc"
        [ "--ghc-arg=-package", "--ghc-arg=parsec"
        , "--ghc-arg=-package", "--ghc-arg=containers"
        , "--ghc-arg=-i" ++ importDir
        , runnerPath
        , parserName
        ]
        content

      -- Clean up
      removeFile runnerPath `catch` (\(_ :: SomeException) -> pure ())

      case result of
        Left (e :: SomeException) ->
          pure $ Left $ GhcRunFailed (show e)
        Right (ExitSuccess, stdout, _) ->
          pure $ Right $ parseOutput stdout
        Right (ExitFailure _, _, stderr) ->
          pure $ Left $ GhcRunFailed stderr

-- | Generate the runner Haskell script
generateRunner :: String -> String
generateRunner moduleName = unlines
  [ "module Main where"
  , ""
  , "import " ++ moduleName ++ " (parsers)"
  , "import Text.Parsec"
  , "import Text.Parsec.String"
  , "import qualified Data.Map as Map"
  , "import System.Environment (getArgs)"
  , "import System.Exit (exitFailure)"
  , "import System.IO (hPutStrLn, stderr)"
  , ""
  , "main :: IO ()"
  , "main = do"
  , "  args <- getArgs"
  , "  case args of"
  , "    [parserName] -> do"
  , "      content <- getContents"
  , "      case Map.lookup parserName parsers of"
  , "        Nothing -> do"
  , "          hPutStrLn stderr $ \"Unknown parser: \" ++ parserName"
  , "          exitFailure"
  , "        Just p -> mapM_ printMatch (findAll p content)"
  , "    _ -> do"
  , "      hPutStrLn stderr \"Usage: runner <parserName>\""
  , "      exitFailure"
  , ""
  , "findAll :: Parser String -> String -> [(Int, Int, String)]"
  , "findAll p = go 1 1"
  , "  where"
  , "    go _ _ [] = []"
  , "    go line col input@(c:cs) ="
  , "      case parse p \"\" input of"
  , "        Right match ->"
  , "          let len = length match"
  , "              newlines = length (filter (=='\\n') match)"
  , "              (newLine, newCol) ="
  , "                if newlines > 0"
  , "                then (line + newlines, length (takeWhile (/='\\n') (reverse match)) + 1)"
  , "                else (line, col + len)"
  , "          in (line, col, match) : go newLine newCol (drop len input)"
  , "        Left _ ->"
  , "          if c == '\\n'"
  , "          then go (line + 1) 1 cs"
  , "          else go line (col + 1) cs"
  , ""
  , "printMatch :: (Int, Int, String) -> IO ()"
  , "printMatch (l, c, m) = putStrLn $ show l ++ \":\" ++ show c ++ \":\" ++ escape m"
  , "  where"
  , "    escape = concatMap (\\x -> if x == '\\n' then \"\\\\n\" else [x])"
  ]

-- | Parse the output from runghc (LINE:COL:MATCH per line)
parseOutput :: String -> [(Int, Int, String)]
parseOutput = map parseLine . filter (not . null) . lines
  where
    parseLine s =
      let (lineStr, rest1) = break (== ':') s
          (colStr, rest2) = break (== ':') (drop 1 rest1)
          matchText = unescape (drop 1 rest2)
      in (read lineStr, read colStr, matchText)

    unescape [] = []
    unescape ('\\':'n':rest) = '\n' : unescape rest
    unescape (c:rest) = c : unescape rest