{-# 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