ackぽい再帰的grepを実装してみました

hck

ソースコード検索の ack がよさげな件: blog.bulknews.netなどで紹介されているackぽいコマンドをHaskellで実装してみました。関数を繋いで繋いで、できるだけHaskellぽい実装になるように気をつけてみました。特にmain関数とか。
ファイル: hck.hs

module Main (main) where

import Control.Monad (when)
import Data.List (isSuffixOf)
import System (getArgs)
import System.Exit (exitWith, ExitCode(ExitFailure))
import System.Directory (getDirectoryContents, doesDirectoryExist)
import System.IO (readFile)
import Text.Printf (printf)
import Text.Regex (mkRegex, matchRegex, subRegex, Regex)

getDirectoryFilesRecursive :: FilePath -> IO [FilePath]
getDirectoryFilesRecursive dir =
  return . concat =<< mapM trav . notDot =<< getDirectoryContents dir
  where
    notDot :: [FilePath] -> [FilePath]
    notDot = filter (\ f -> all (/= f) [".", ".."])

    trav :: FilePath -> IO [FilePath]
    trav file = do
      let path = dir ++ "/" ++ file
      isDir <- doesDirectoryExist path
      if isDir then getDirectoryFilesRecursive path
               else return [path]

ignoreExt :: [FilePath] -> [FilePath]
ignoreExt = 
  let exts = [".o", ".exe", ".hi", "~", ".bak", "/CVS", "/RCS", "/.svn"]
  in filter (\ f -> not $ any (flip isSuffixOf f) exts)

type FileContents = (FilePath, [(Int, String)])

readContentsFromFiles :: [FilePath] -> IO [FileContents]
readContentsFromFiles files =
  return . zip files . map (zip [1 ..] . lines) =<< mapM readFile files

grep :: Regex -> [FileContents] -> [FileContents]
grep pat = 
  filter (\ (f, ls) -> not $ null ls) .
    map (\ (f, ls) -> (f, filter match ls))
  where
    match :: (Int, String) -> Bool
    match (n, l) =
      case matchRegex pat l of
        Just s  -> True
        Nothing -> False

format :: Regex -> [FileContents] -> [String]
format pat =
  concatMap (\ (f, ls) -> green f : map format' ls)
  where
    format' :: (Int, String) -> String
    format' (n, l) = printf "%6d:%s" n $ yellow l

    green :: String -> String
    green s = "\x1b[1;32;40m" ++ s ++ "\x1b[0m"

    yellow :: String -> String
    yellow l = subRegex pat l "\x1b[30;43m\\1\x1b[0m"

main :: IO ()
main = do
  (pat, dir) <- parseArgs =<< getArgs
  mapM_ putStrLn . format pat . grep pat =<<
    readContentsFromFiles . ignoreExt =<<
    getDirectoryFilesRecursive dir
  where
    usage :: IO ()
    usage = do
      putStrLn "usage: hck [pattern] [dir]"
      exitWith (ExitFailure 1)

    parseArgs :: [String] -> IO (Regex, FilePath)
    parseArgs args = do
      when (null args) $ usage
      let pat = mkRegex $ "(" ++ args !! 0 ++ ")"
          dir = if (length args > 1) then args !! 1 else "."
      return (pat, dir)

-- Local Variables:
-- compile-command: "ghc -W -fno-warn-unused-matches --make -o hck hck"
-- End:

BUGS: オプションがまったく実装されてません。
参照: WIN32OLEでiPod Shuffleもどき - 趣味的にっき