crux-0.7: src/Crux/Config/Load.hs
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE DataKinds #-}
{-# Language DeriveGeneric, MultiWayIf, OverloadedStrings #-}
-- | This module deals with loading configurations.
module Crux.Config.Load where
import Control.Lens (set)
import Control.Monad(foldM, (<=<))
import Control.Exception(Exception(..),catch,catches,throwIO, Handler(..))
import Data.Generics.Product.Fields (field, setField)
import Data.Text (Text)
import GHC.Generics (Generic)
import System.Environment
import SimpleGetOpt
import Config
import Config.Schema
import Config.Schema.Load.Error(ErrorAnnotation(..))
import Crux.Config
-- | The result of loading a configuration.
data Options opts =
ShowHelp {- XXX: Add help strings -} -- ^ Show help and exit
| ShowVersion -- ^ Show version and exit
| Options opts [FilePath] -- ^ We loaded some options
data ColorOptions = ColorOptions
{ noColorsErr :: Bool
, noColorsOut :: Bool
}
deriving (Generic)
defaultColorOptions :: ColorOptions
defaultColorOptions = allColors
allColors :: ColorOptions
allColors = ColorOptions
{ noColorsErr = False
, noColorsOut = False
}
noColors :: ColorOptions
noColors = ColorOptions
{ noColorsErr = True
, noColorsOut = True
}
-- | Command line options processed before loading the configuration file.
data EarlyConfig opts = EarlyConfig
{ showHelp :: Bool -- ^ Describe options & quit
, showVersion :: Bool -- ^ Show tool version & quit
, configFile :: Maybe FilePath
-- ^ Load configuratoin from here.
-- Other command line options override the settings in the file.
, colorOptions :: ColorOptions
, options :: OptSetter opts
, files :: [FilePath]
}
deriving (Generic)
commandLineOptions :: Config opts -> OptSpec (EarlyConfig opts)
commandLineOptions cfg = OptSpec
{ progDefaults = EarlyConfig
{ showHelp = False
, showVersion = False
, configFile = Nothing
, colorOptions = defaultColorOptions
, options = Right
, files = []
}
, progOptions =
[ Option "h?" ["help"]
"Print this help message"
$ NoArg $ \opts -> Right opts { showHelp = True }
, Option "V" ["version"]
"Show the version of the tool"
$ NoArg $ \opts -> Right opts { showVersion = True }
, Option "" ["config"]
"Load configuration from this file."
$ ReqArg "FILE" $ \f opts -> Right opts { configFile = Just f }
, Option [] ["no-colors-err"]
"Suppress color codes in the errors"
$ NoArg $ Right . set (field @"colorOptions" . field @"noColorsErr") True
, Option [] ["no-colors-out"]
"Suppress color codes in the output"
$ NoArg $ Right . set (field @"colorOptions" . field @"noColorsOut") True
, Option [] ["no-colors"]
"Suppress color codes in both the output and the errors"
$ NoArg $ Right . setField @"colorOptions" noColors
] ++ map (mapOptDescr delayOpt) (cfgCmdLineFlag cfg)
, progParamDocs = [("FILES", "Input files to process.")]
, progParams = \f opts -> Right opts { files = f : files opts }
}
delayOpt :: OptSetter opts -> OptSetter (EarlyConfig opts)
delayOpt f opts = Right opts { options = f <=< options opts }
data ConfigFileLoc =
NoConfgFile
| AtPosition Position
deriving Show
instance ErrorAnnotation ConfigFileLoc where
displayAnnotation a =
case a of
NoConfgFile -> "(no configuration file)"
AtPosition p -> displayAnnotation p
data ConfigError =
FailedToReadFile IOError
| FailedToParseFile ParseError
| FailedToProcessFile (ValueSpecMismatch ConfigFileLoc)
| InvalidEnvVar String String String -- ^ variable, value, error message
| InvalidCommandLine [String]
deriving Show
instance Exception ConfigError where
displayException = ppConfigError
ppConfigError :: ConfigError -> String
ppConfigError (FailedToReadFile ioe) =
"Failed to read config file: " ++ displayException ioe
ppConfigError (FailedToParseFile pe) =
"Failed to parse config file: " ++ displayException pe
ppConfigError (FailedToProcessFile vsm) =
"Failed to check config file: " ++ displayException vsm
ppConfigError (InvalidEnvVar var val msg) =
unwords ["Environment variable", var, "has invalid value", val ++ ":", msg]
ppConfigError (InvalidCommandLine msg) =
unlines ("Invalid command line option:" : msg)
-- | Merges command-line options, environment variable options, and
-- configuration file options (in that order) to get the overall
-- Options configuration for running Crux. Throws 'ConfigError' on
-- failure.
loadConfig :: Text -> Config opts -> IO (ColorOptions, Options opts)
loadConfig nm cfg =
do earlyOpts <- getOptsX (commandLineOptions cfg) `catch`
\(GetOptException errs) -> throwIO (InvalidCommandLine errs)
let copts = colorOptions earlyOpts
if | showHelp earlyOpts -> pure (copts, ShowHelp)
| showVersion earlyOpts -> pure (copts, ShowVersion)
| otherwise ->
do opts <- fromFile nm cfg (configFile earlyOpts)
opts1 <- foldM fromEnv opts (cfgEnv cfg)
case options earlyOpts opts1 of
Left err -> throwIO (InvalidCommandLine [err])
Right opts2 -> pure (copts, Options opts2 (reverse (files earlyOpts)))
-- | Load settings from a file, or from an empty configuration value.
fromFile :: Text -> Config opts -> Maybe FilePath -> IO opts
fromFile nm cfg mbFile =
do let spec = sectionsSpec nm (cfgFile cfg)
case mbFile of
Nothing -> -- no file, use an empty value
case loadValue spec (Sections NoConfgFile []) of
Left err -> throwIO (FailedToProcessFile err)
Right opts -> pure opts
Just file ->
loadValueFromFile spec file
`catches` [ Handler (throwIO . FailedToReadFile)
, Handler (throwIO . FailedToParseFile)
, Handler (throwIO . FailedToProcessFile)
]
-- | Modify the options using an environment variable.
fromEnv :: opts -> EnvDescr opts -> IO opts
fromEnv opts v =
do mb <- lookupEnv (evName v)
case mb of
Just s -> case evValue v s opts of
Right opts1 -> pure opts1
Left err -> throwIO (InvalidEnvVar (evName v) s err)
Nothing -> pure opts