{-# LANGUAGE ScopedTypeVariables #-} {-| Module : System.Environment.MrEnv Description : Read environment variables, with default fallbacks Copyright : 2020 Christian Rocha License : MIT Maintainer : christian@rocha.is Stability : experimental Portability : POSIX A simple way to read environment variables. -} module System.Environment.MrEnv ( {-| Read environment variables with fallback values. A simple example with @do@ notation: @ import System.Environment.MrEnv ( envAsBool, envAsInt, envAsInteger, envAsString ) main :: IO () main = do -- Get a string, with a fallback value if nothing is set. host <- envAsString \"HOST\" "localhost" -- Get an int. If you need an integer instead you could also use envAsInteger. port <- envAsInt \"PORT\" 8000 -- Get a boolean. Here we're expecting the environment variable to read -- something along the lines of "true", \"TRUE\", \"True\", "truE" and so on. debug <- envAsBool \"DEBUG\" False putStrLn $ "Let's connect to " ++ host ++ " on port " ++ show port ++ ". Debug mode is " ++ if debug then "on" else "off" ++ "." @ You can also read into a record: @ import System.Environment.MrEnv ( envAsBool, envAsInt, envAsInteger, envAsString ) data Config = Config { host :: String , port :: Int , debug :: Bool } getConfig :: IO Config getConfig = Config \<$\> envAsString \"HOST\" "localhost" \<*\> envAsInt \"PORT\" 8000 \<*\> envAsBool \"DEBUG\" False main :: IO () main = getConfig >>= \conf -> putStrLn $ "Let's connect to " ++ host conf ++ " on port " ++ show $ port conf ++ ". Debug mode is " ++ if debug conf then "on" else "off" ++ "." @ -} envAsBool , envAsInt , envAsInteger , envAsString ) where import Control.Exception ( catch ) import Data.Char ( toLower, toUpper ) import Data.Maybe ( fromMaybe ) import System.Environment ( getEnv ) import Text.Read ( readMaybe ) {-| Get an environment variable, with a fallback value and the ability to preprocess the raw string before @read@ing it. -} envAs' :: forall a. Read a => String -- ^Name of environment variable -> (String -> Maybe a) -- ^Preprocessing function -> a -- ^Fallback value -> IO a -- ^Result envAs' name prep defaultValue = catch (fromMaybe defaultValue . prep <$> getEnv name) ((const $ pure defaultValue) :: IOError -> IO a) {-| Get an environment variable, with a fallback value. -} envAs :: forall a. Read a => String -- ^Name of environment variable -> a -- ^Fallback value -> IO a -- ^Result envAs name = envAs' name readMaybe {-| Get an environment variable as a @'String'@, with a fallback value. Internally we use this instead of @'envAs' \@String@, because 'readMaybe' fails unless 'String's are doubly-quoted (i.e. '"\"value\""'. -} envAsString :: String -- ^Name of environment variable -> String -- ^Fallback value -> IO String -- ^Result envAsString name = envAs' name (readMaybe . (\v -> "\"" ++ v ++ "\"")) {-| Get an environment variable as an @'Int'@, with a fallback value. -} envAsInt :: String -- ^Name of environment variable -> Int -- ^Fallback value -> IO Int -- ^Result envAsInt = envAs {-| Get an environment variable as an @'Integer'@, with a fallback value. -} envAsInteger :: String -- ^Name of environment variable -> Integer -- ^Fallback value -> IO Integer -- ^Result envAsInteger = envAs {-| Get an environment variable as a boolean, with a fallback value. Internally we use this instead of @'envAs' \@Bool@, as it handles nonstandard capitalization. -} envAsBool :: String -- ^Name of environment variable -> Bool -- ^Fallback value -> IO Bool -- ^Result envAsBool name = envAs' name (readMaybe . capitalize) {-| Capitalize the first character in a string and make all other characters lowercase. In our case we're doing this so values like like TRUE, true, True, and truE all become "True," which can then be coerced to a boolean. -} capitalize :: String -> String capitalize [] = [] capitalize (head':tail') = toUpper head' : map toLower tail'