judge-0.1.2.0: app/CLI.hs
{-|
Module : CLI
Description : Options to judge's command-line interface.
Copyright : (c) 2017, 2018 N Steenbergen
License : GPL-3
Maintainer : ns@slak.ws
Stability : experimental
-}
{-# LANGUAGE PackageImports #-}
module CLI where
import Prelude hiding (getContents)
import "base" Data.Char (toLower)
import "base" Data.List (intercalate)
import "base" Data.Monoid ((<>))
import "base" Data.Version (showVersion)
import "base" Control.Applicative ((*>),(<*),(<|>))
import "base" Control.Monad (void, forM)
import "base" System.Info (os)
import "base" System.IO (FilePath, IOMode(WriteMode))
import "base" System.IO.Error (catchIOError)
import "base" GHC.IO.Handle (Handle, hIsTerminalDevice)
import "base" GHC.IO.Handle.FD (stdout, stderr, stdin, openFile)
import "text" Data.Text (Text, pack, unpack)
import "text" Data.Text.IO (getContents)
import qualified "filepath" System.FilePath as FP
import qualified "directory" System.Directory as D
import qualified "optparse-applicative" Options.Applicative as O
import qualified "attoparsec" Data.Attoparsec.Text as P
import qualified "ansi-wl-pprint" Text.PrettyPrint.ANSI.Leijen as PP
import Paths_judge (getDataDir, version) -- automatically generated
import Logic.Judge.Formula.Parser (parse, Parseable)
import qualified Logic.Judge.Writer as W
data Arguments = Arguments
{ verbose :: Bool
, _goals :: [String]
, _assumptions :: [String]
, _outfile :: Maybe String
, format :: W.Format
, _infile :: String
}
arguments :: IO Arguments
arguments = do
logics <- map FP.takeBaseName <$> findLogics
O.execParser (prog logics)
where
prog logics = O.info
( O.helper <*> infoOptions logics <*> runOptions )
( O.fullDesc
<> O.progDescDoc (return description)
<> O.header "judge - Decision procedure for formal logics"
<> O.footer "2017-2018, Utrecht University"
)
infoOptions logics =
O.infoOption
("judge - version " ++ showVersion version)
( O.short 'V'
<> O.long "version"
<> O.help "Show version of the software and exit"
)
<*> O.infoOption
("Available logical systems: " ++ intercalate ", " logics)
( O.short 'l'
<> O.long "list-logics"
<> O.help "List logical systems available in the data \n\
\directories and exit"
)
runOptions = Arguments
<$> O.switch
( O.short 'v'
<> O.long "verbose"
<> O.help "Show diagnostics"
)
<*> O.many
( O.strOption
( O.short 'g'
<> O.long "goal"
<> O.metavar "EXPR"
<> O.help "Set target formula(s). May be provided multiple \n\
\times (default: standard input)"
)
)
<*> O.many
( O.strOption
( O.short 'a'
<> O.long "assumption"
<> O.metavar "EXPR"
<> O.help "Add assumption(s). May be provided multiple times"
)
)
<*> O.optional
( O.strOption
( O.short 'o'
<> O.long "output"
<> O.metavar "PATH"
<> O.help "Output file (default: standard output)"
)
)
<*> ( O.option O.auto
( O.short 'f'
<> O.long "format"
<> O.metavar "FORMAT"
<> O.value W.Plain
<> O.showDefault
<> O.help "Output format"
)
)
<*> ( O.argument O.str (O.metavar "LOGIC"))
description :: PP.Doc
description =
(PP.line <>)
. PP.fillSep
. map PP.text
. words
$ "Decides whether given logical formulas are provable in some \n\
\logical system. Takes a YAML or JSON file as input. Refer to \n\
\README.md for more information."
-- | Find all paths to logics in the resource directories.
findLogics :: IO [FilePath]
findLogics = do
dat <- (FP.</> "logic") <$> getDataDir
xdg <- D.getXdgDirectory D.XdgData "judge"
findFiles ["json", "yml", "yaml"] [xdg, dat]
-- | Find all files with the given extensions in the given directories. If a
-- directory does not exist or has permission issues, it is simply ignored.
findFiles :: [String] -> [FilePath] -> IO [FilePath]
findFiles extensions dirs = concat <$> forM dirs findFiles
where
-- | Find all relevant files in the given directory.
findFiles :: FilePath -> IO [FilePath]
findFiles dir = do
{ files <- filter correctExtension <$> D.listDirectory dir
; return $ map (dir FP.</>) files
} `catchIOError` (\e -> return [])
-- | Check if the filepath has any of the accepted extensions.
correctExtension :: FilePath -> Bool
correctExtension path = flip any extensions $
(/= Nothing) .
flip FP.stripExtension (map toLower path)
-- | Return input file name, or, if it doesn't exist, check if it matches any
-- name in the resource directories.
infile :: Arguments -> IO String
infile args = do
let name = _infile args
exists <- D.doesFileExist name
if exists
then return name
else do
logics <- filter (match name) <$> findLogics
case logics of
(filename:_) -> return filename
_ -> return name
where
match :: String -> FilePath -> Bool
match name path = name `elem` [FP.takeBaseName path, FP.takeFileName path]
-- | Obtain and open file handle for output file.
outfile :: Arguments -> IO Handle
outfile arg = maybe (return stdout) (flip openFile WriteMode) (_outfile arg)
-- | Obtain additional assumptions. Taken from command line arguments.
assumptions :: Parseable f => Arguments -> IO [f]
assumptions arg = mapM (parse . pack) (_assumptions arg)
-- | Obtain goal formulas. Taken from command line arguments or standard input.
goals :: Parseable f => Arguments -> IO [f]
goals arg = case _goals arg of
[] -> do
terminal <- hIsTerminalDevice stdin
if terminal
then W.prettyprint stderr notification
else return ()
getContents >>= parse
xs -> mapM (parse . pack) xs
where
notification :: PP.Doc
notification =
PP.text "Reading formulas from standard input" PP.<+>
PP.lparen PP.<>
eof PP.<+>
PP.text "to finish" PP.<>
PP.rparen
eof :: PP.Doc
eof = PP.bold . PP.text $ case os of
"windows" -> "CTRL-Z"
_ -> "CTRL-D"