packages feed

hwk-0.6: src/Main.hs

{-# LANGUAGE CPP #-}

#if !MIN_VERSION_simple_cmd_args(0,1,4)
import Control.Applicative (
#if !MIN_VERSION_simple_cmd_args(0,1,3)
    (<|>),
#endif
    many)
#endif
import Control.Monad.Extra
import qualified Data.List.Extra as L
import Data.Version (showVersion)
import Language.Haskell.Interpreter
import SimpleCmdArgs
import System.Directory
import System.FilePath

import Common
import Hwk.IO
import Hwk.Types
import Paths_hwk (getDataDir, version)

data HwkMode = DefaultMode | LineMode | WordsMode | WholeMode | TypeMode | EvalMode | RunMode | ShellMode
  deriving Eq

main :: IO ()
main = do
  userdir <- getXdgDirectory XdgConfig "hwk"
  simpleCmdArgs (Just version) "A Haskell awk/sed like tool"
    "Simple shell text processing with Haskell" $
    runExpr userdir <$> modeOpt <*> cfgdirOpt userdir <*> strArg "FUNCTION" <*> many (strArg "FILE...")
  where
    modeOpt :: Parser HwkMode
    modeOpt =
      flagWith' LineMode 'l' "line" "Apply function to each line" <|>
      flagWith' WordsMode 'w' "words" "Apply function to list of words per line" <|>
      flagWith' WholeMode 'a' "all" "Apply function once to the whole input" <|>
      flagWith' TypeMode 't' "typecheck" "Print out the type of the given function" <|>
      flagWith' EvalMode 'e' "eval" "Evaluate a Haskell expression" <|>
      flagWith' RunMode 'r' "run" "Run Haskell IO" <|>
      flagWith DefaultMode ShellMode 's' "shell" "Haskell Shell"

    cfgdirOpt :: String -> Parser (Maybe FilePath)
    cfgdirOpt dir =
      optional (normalise <$> strOptionWith 'c' "config-dir" "DIR" ("Override the config dir [default:" ++ dir ++ "]"))

runExpr :: FilePath -> HwkMode -> Maybe FilePath -> String -> [FilePath] -> IO ()
runExpr userdir mode mcfgdir stmt files = do
  mapM_ checkFileExists files
  cfgdir <-
    case mcfgdir of
      Just dir -> do
        unlessM (doesDirectoryExist dir) $
          error' $ dir ++ ": directory not found"
        return dir
      Nothing -> do
        let usercfg = userdir </> "Hwk.hs"
        datadir <- getDataDir
        let datafile = datadir </> "Hwk.hs"
        unlessM (doesFileExist datafile) $
          error' $ "default config file does not exist: " ++ datafile
        createDirectoryIfMissing True userdir
        let versionCfg = usercfg ++ "-" ++ showVersion version
        unlessM (doesFileExist versionCfg) $
          copyFile datafile versionCfg
        unlessM (doesFileExist usercfg) $ do
          copyFile datafile usercfg
          warn $ usercfg ++ " created"
        return userdir
  runInterpreter (runHint cfgdir) >>=
    either (putStrLn . errorString) return
  where
    runHint :: FilePath -> Interpreter ()
    runHint cfgdir = do
      -- could ignore this for eval or typecheck
      set [searchPath := [cfgdir], languageExtensions := [TypeApplications]]
      loadModules ["Hwk"]
      let setHwkImports ms = setImports (L.nub (ms ++ ["Hwk"]))
      setHwkImports ["Prelude"]
      imports <- do
        haveModules <- typeChecks "userModules"
        if haveModules
          then interpret "userModules" infer
          else return ["Prelude", "Data.List"]
      setHwkImports imports
      let inputs = if null files then ["-"] else files
      case mode of
        DefaultMode ->
          withInputFiles inputs (mapInputList stmt . lines)
        LineMode ->
          withInputFiles inputs (mapEachLine stmt . lines)
        WordsMode ->
          withInputFiles inputs (mapWordsLine stmt . map words . lines)
        WholeMode -> do
          withInputFiles inputs (applyToInput stmt . removeTrailingNewline)
        -- FIXME take or warn about args
        TypeMode -> typeOfExpr stmt
        EvalMode -> evalExpr stmt
        RunMode -> execExpr stmt
        ShellMode -> liftIO setNoBuffering >> shellSession
      where
        withInputFiles :: [FilePath] -> (String -> Interpreter ())
                       -> Interpreter ()
        withInputFiles inputs interp = do
          forM_ inputs $ \ file ->
            liftIO (if file == "-" then getContents else readFile file) >>=
            interp

        removeTrailingNewline :: String -> String
        removeTrailingNewline "" = ""
        removeTrailingNewline s =
          if last s == '\n'
          then init s
          else s

    checkFileExists :: FilePath -> IO ()
    checkFileExists "-" = return ()
    checkFileExists file = do
      unlessM (doesFileExist file) $
        error' $ "file not found: " ++ file

    errorString :: InterpreterError -> String
    errorString (WontCompile es) =
      unlines $ "ERROR: Won't compile:" : map errMsg es
    errorString e = show e

-- fn $ lines input
mapInputList :: String -> [String] -> Interpreter ()
mapInputList stmt inputs = do
  typ <- resultTypeOfApplied stmt "[String]"
  case cleanupType typ of
    "String" -> do
      fn <- interpret stmt (as :: [String] -> String)
      liftIO $ putStrLn (fn inputs)
    "[String]" -> do
      fn <- interpret stmt (as :: [String] -> [String])
      liftIO $ mapM_ putStrLn (fn inputs)
    "[[String]]" -> do
      fn <- interpret stmt (as :: [String] -> [[String]])
      liftIO $ mapM_ (putStrLn . unwords) (fn inputs)
    "Int" -> do
      fn <- interpret stmt (as :: [String] -> Int)
      liftIO $ print (fn inputs)
    "[Int]" -> do
      fn <- interpret stmt (as :: [String] -> [Int])
      liftIO $ mapM_ print (fn inputs)
    "[[Int]]" -> do
      fn <- interpret stmt (as :: [String] -> [[Int]])
      liftIO $ mapM_ (putStrLn . unwords . map show) (fn inputs)
    _ -> do
      liftIO $ warn typ
      fn <- interpret stmt (as :: [String] -> [String])
      liftIO $ mapM_ putStrLn (fn inputs)

-- map fn $ lines input
mapEachLine :: String -> [String] -> Interpreter ()
mapEachLine stmt inputs = do
  typ <- resultTypeOfApplied stmt "String"
  case cleanupType typ of
    "String" -> do
      fn <- interpret stmt (as :: String -> String)
      liftIO $ mapM_ (putStrLn . fn) inputs
    "[String]" -> do
      fn <- interpret stmt (as :: String -> [String])
      liftIO $ mapM_ (putStrLn . unwords . fn) inputs
    "[[String]]" -> do
      fn <- interpret stmt (as :: String -> [[String]])
      liftIO $ mapM_ (putStrLn . L.intercalate "\t" . map unwords . fn) inputs
    "Int" -> do
      fn <- interpret stmt (as :: String -> Int)
      liftIO $ mapM_ (print . fn) inputs
    "[Int]" -> do
      fn <- interpret stmt (as :: String -> [Int])
      liftIO $ mapM_ (putStrLn . unwords . map show . fn) inputs
    "[[Int]]" -> do
      fn <- interpret stmt (as :: String -> [[Int]])
      liftIO $ mapM_ (putStrLn . L.intercalate "\t" . map (unwords . map show) .fn) inputs
    _ -> do
      liftIO $ warn typ
      fn <- interpret stmt (as :: String -> String)
      liftIO $ mapM_ (putStrLn . fn) inputs

-- map fn $ lines input
mapWordsLine :: String -> [[String]] -> Interpreter ()
mapWordsLine stmt inputs = do
  typ <- resultTypeOfApplied stmt "[String]"
  case cleanupType typ of
    "String" -> do
      fn <- interpret stmt (as :: [String] -> String)
      liftIO $ mapM_ (putStrLn . fn) inputs
    "[String]" -> do
      fn <- interpret stmt (as :: [String] -> [String])
      liftIO $ mapM_ (putStrLn . unwords . fn) inputs
    "[[String]]" -> do
      fn <- interpret stmt (as :: [String] -> [[String]])
      liftIO $ mapM_ (putStrLn . L.intercalate "\t" . map unwords . fn) inputs
    "Int" -> do
      fn <- interpret stmt (as :: [String] -> Int)
      liftIO $ mapM_ (print . fn) inputs
    "[Int]" -> do
      fn <- interpret stmt (as :: [String] -> [Int])
      liftIO $ mapM_ (putStrLn . unwords . map show . fn) inputs
    "[[Int]]" -> do
      fn <- interpret stmt (as :: [String] -> [[Int]])
      liftIO $ mapM_ (putStrLn . L.intercalate "\t" . map (unwords . map show) .fn) inputs
    _ -> do
      liftIO $ warn typ
      fn <- interpret stmt (as :: [String] -> [String])
      liftIO $ mapM_ (putStrLn . unwords . fn) inputs

-- fn input
applyToInput :: String -> String -> Interpreter ()
applyToInput stmt input = do
  typ <- resultTypeOfApplied stmt "String"
  case cleanupType typ of
    "String" -> do
      fn <- interpret stmt (as :: String -> String)
      liftIO $ putStrLn (fn input)
    "[String]" -> do
      fn <- interpret stmt (as :: String -> [String])
      liftIO $ mapM_ putStrLn (fn input)
    "[[String]]" -> do
      fn <- interpret stmt (as :: String -> [[String]])
      liftIO $ mapM_ (putStrLn . unwords) (fn input)
    "Int" -> do
      fn <- interpret stmt (as :: String -> Int)
      liftIO $ print (fn input)
    "[Int]" -> do
      fn <- interpret stmt (as :: String -> [Int])
      liftIO $ mapM_ print (fn input)
    "[[Int]]" -> do
      fn <- interpret stmt (as :: String -> [[Int]])
      liftIO $ mapM_ (putStrLn . unwords . map show) (fn input)
    _ -> do
      liftIO $ warn typ
      fn <- interpret stmt (as :: String -> [String])
      liftIO $ mapM_ putStrLn (fn input)

resultTypeOfApplied :: MonadInterpreter m => String -> String -> m String
resultTypeOfApplied expr typ =
  L.dropPrefix (typ ++ " -> ") <$> typeOf (expr ++ " . (id  @" ++ typ ++ ")")

typeOfExpr :: String -> Interpreter ()
typeOfExpr stmt = do
  etypchk <- typeChecksWithDetails stmt
  case etypchk of
    Left err -> liftIO $ mapM_ (putStrLn . errMsg) err
    Right typ -> liftIO $ putStrLn (cleanupType typ)

evalExpr :: String -> Interpreter ()
evalExpr stmt = do
  typ <- typeOf stmt
  case cleanupType typ of
    "String" -> do
      interpret stmt "a String" >>= liftIO . putStrLn
    "[String]" -> do
      interpret stmt ["a String"] >>= liftIO . mapM_ putStrLn
    "[[String]]" -> do
      interpret stmt [["a String"]] >>= liftIO . mapM_ (putStrLn . unwords)
    "Int" -> do
      interpret stmt (1 :: Int) >>= liftIO . print
    "[Int]" -> do
      interpret stmt [1 :: Int] >>= liftIO . mapM_ print
    "[[Int]]" -> do
      interpret stmt [[1 :: Int]] >>= liftIO . mapM_ (putStrLn . unwords . map show)
    _ ->
      if " -> " `L.isInfixOf` typ
        then liftIO $ putStrLn typ
        else
        -- FIXME option to display type
        eval stmt >>= liftIO . putStrLn

-- from simple-cmd
error' :: String -> a
#if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,9,0))
error' = errorWithoutStackTrace
#else
error' = error
#endif