packages feed

dhall-text-shell-0.1.0.0: src/Dhall/TextShell.hs

{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NamedFieldPuns    #-}
{-# LANGUAGE RecordWildCards   #-}

module Dhall.TextShell (main) where

import Control.Exception (Handler (..), SomeException)
import Control.Monad (foldM)
import Control.Applicative (optional)
import Data.Text (Text)
import Data.Void (Void)
import Dhall.Core (Expr(Annot))
import Dhall.Import (SemanticCacheMode (..), Imported(..))
import Dhall.Parser (Src)
import Dhall.TypeCheck (Censored (..), DetailedTypeError (..), TypeError)
import Dhall.Util (Input (..), Output (..), Censor(..))
import Options.Applicative (Parser)
import System.Exit (ExitCode, exitFailure)
import qualified Control.Exception
import qualified Data.Map
import qualified Data.Text
import qualified Data.Text.IO
import qualified Dhall
import qualified Dhall.Core
import qualified Dhall.Import
import qualified Dhall.TypeCheck
import qualified Dhall.Util
import qualified GHC.IO.Encoding
import qualified Options.Applicative
import qualified System.FilePath
import qualified System.IO
import qualified System.Process

-- | Options from general dhall tools
data AsCommand = AsCommand
    { censor  :: Censor
    , explain :: Bool
    }

-- | Options specifically for TextShell
data Options = Options
    { file    :: Input
    , output  :: Output
    , argCmds :: [String]
    }

-- | Parse 'Options' and 'AsCommand'
parseConfig :: Parser (Options, AsCommand)
parseConfig = (,) <$> parseOptions
                  <*> parseAsCommand
  where
    switch name description =
        Options.Applicative.switch
            (   Options.Applicative.long name
            <>  Options.Applicative.help description
            )
    parseOptions =
      Options   <$> parseFile
                <*> parseOutput
                <*> Options.Applicative.many parseArgCmd
    parseFile = fmap f (optional p)
      where
        f  Nothing    = StandardInput
        f (Just file) = InputFile file

        p = Options.Applicative.strOption
                (   Options.Applicative.long "file"
                <>  Options.Applicative.help "Read expression from a file instead of standard input"
                <>  Options.Applicative.metavar "FILE"
                <>  Options.Applicative.action "file"
                )
    parseOutput = fmap f (optional p)
      where
        f Nothing = StandardOutput
        f (Just file) = OutputFile file

        p = Options.Applicative.strOption
                (   Options.Applicative.long "output"
                <>  Options.Applicative.help "Write result to a file instead of standard output"
                <>  Options.Applicative.metavar "FILE"
                <>  Options.Applicative.action "file"
                )
    parseArgCmd = Options.Applicative.strOption
            (   Options.Applicative.long "argCmd"
            <>  Options.Applicative.help "Use shell command to supply as `Text -> Text` argument"
            <>  Options.Applicative.metavar "CMD"
            <>  Options.Applicative.action "CMD"
            )
    parseAsCommand =
      AsCommand <$> parseCensor
                <*> switch "explain" "Explain error messages in more detail"
    parseCensor = fmap f (switch "censor" "Hide source code in error messages")
      where
        f True  = Censor
        f False = NoCensor

main :: IO ()
main = do
    (options, ac) <- Options.Applicative.execParser $
      Options.Applicative.info
          (Options.Applicative.helper <*> parseConfig)
          (   Options.Applicative.progDesc "render dhall text with shell commands as function arguments"
          <>  Options.Applicative.fullDesc
          )
    runWithOptions ac options

runWithOptions :: AsCommand -> Options -> IO ()
runWithOptions ac Options{..} = asCommand ac $ \getExpression rootDirectory -> do
    expression <- getExpression file

    resolvedExpression <-
        Dhall.Import.loadRelativeTo (rootDirectory file) UseSemanticCache expression

    let addPiLayer :: Expr Src Void -> Expr Src Void
        addPiLayer = Dhall.Core.Pi
          Nothing "_"
          (Dhall.Core.Pi Nothing "_" Dhall.Core.Text Dhall.Core.Text)
        expectedType = iterate addPiLayer Dhall.Core.Text !! length argCmds
    _ <- Dhall.Core.throws (Dhall.TypeCheck.typeOf (Annot resolvedExpression expectedType))

    let normalizedExpression = Dhall.Core.normalize resolvedExpression
        peelArg :: (Expr Void Void, Data.Map.Map Text [String])
                -> String
                -> Maybe (Expr Void Void, Data.Map.Map Text [String])
        peelArg (currExp, currMap) arg = case currExp of
          Dhall.Core.Lam _ (Dhall.Core.FunctionBinding { functionBindingVariable }) subExp ->
            Just (subExp, Data.Map.insertWith (++) functionBindingVariable [arg] currMap)
          _ -> Nothing
        peeledExprAndMap =
            foldM peelArg (normalizedExpression, Data.Map.empty) argCmds

    case peeledExprAndMap of
      Nothing -> pure () -- this should have been caught during the typecheck
      Just (expr, argMap) -> do
        res <- Dhall.Core.normalizeWithM
          (\x -> case x of
            Dhall.Core.App (Dhall.Core.Var (Dhall.Core.V v i))
                  (Dhall.Core.TextLit (Dhall.Core.Chunks [] txt))
              | Just as <- Data.Map.lookup v argMap
              , a:_     <- drop i as
              -> Just <$> do
                sysOut <- Data.Text.pack <$> System.Process.readCreateProcess
                  (System.Process.shell a)
                  (Data.Text.unpack txt)
                pure $ Dhall.Core.TextLit (Dhall.Core.Chunks [] sysOut)
            _ -> pure Nothing
          )
          expr
        case res of
          Dhall.Core.TextLit (Dhall.Core.Chunks [] text) ->
              let write = case output of
                    StandardOutput -> Data.Text.IO.putStr
                    OutputFile file_ -> Data.Text.IO.writeFile file_
              in write text
          _ -> do
              let invalidDecoderExpected :: Expr Void Void
                  invalidDecoderExpected = Dhall.Core.Text

              let invalidDecoderExpression :: Expr Void Void
                  invalidDecoderExpression = res

              Control.Exception.throwIO (Dhall.InvalidDecoder {..})

-- | Copy as much as possible the setup in "Dhall.Main".  If that module
-- changes, this should update as well.
asCommand
    :: AsCommand
    -> ((Input -> IO (Expr Src Dhall.Core.Import)) -> (Input -> FilePath) -> IO ())
    -> IO ()
asCommand AsCommand{..} act = do
    GHC.IO.Encoding.setLocaleEncoding System.IO.utf8

    let rootDirectory = \case
            InputFile f   -> System.FilePath.takeDirectory f
            StandardInput -> "."

    let getExpression = Dhall.Util.getExpression censor

    let handle io =
            Control.Exception.catches io
                [ Handler handleTypeError
                , Handler handleImported
                , Handler handleExitCode
                ]
          where
            handleAll e = do
                let string = show (e :: SomeException)

                if not (null string)
                    then System.IO.hPutStrLn System.IO.stderr string
                    else return ()

                System.Exit.exitFailure

            handleTypeError e = Control.Exception.handle handleAll $ do
                let _ = e :: TypeError Src Void
                System.IO.hPutStrLn System.IO.stderr ""
                if explain
                    then
                        case censor of
                            Censor   -> Control.Exception.throwIO (CensoredDetailed (DetailedTypeError e))
                            NoCensor -> Control.Exception.throwIO (DetailedTypeError e)

                    else do
                        Data.Text.IO.hPutStrLn System.IO.stderr "\ESC[2mUse \"dhall --explain\" for detailed errors\ESC[0m"
                        case censor of
                            Censor   -> Control.Exception.throwIO (Censored e)
                            NoCensor -> Control.Exception.throwIO e

            handleImported (Imported ps e) = Control.Exception.handle handleAll $ do
                let _ = e :: TypeError Src Void
                System.IO.hPutStrLn System.IO.stderr ""
                if explain
                    then Control.Exception.throwIO (Imported ps (DetailedTypeError e))
                    else do
                        Data.Text.IO.hPutStrLn System.IO.stderr "\ESC[2mUse \"dhall --explain\" for detailed errors\ESC[0m"
                        Control.Exception.throwIO (Imported ps e)

            handleExitCode e =
                Control.Exception.throwIO (e :: ExitCode)

    handle $ act getExpression rootDirectory