dotenv 0.9.0.3 → 0.10.0.0
raw patch · 8 files changed
+158/−64 lines, 8 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Configuration.Dotenv: [allowDuplicates] :: Config -> Bool
+ Configuration.Dotenv: [configVerbose] :: Config -> Bool
+ Configuration.Dotenv: ask :: Monad m => ReaderT r m r
+ Configuration.Dotenv: data ReaderT r m a
+ Configuration.Dotenv: liftReaderT :: m a -> ReaderT r m a
- Configuration.Dotenv: Config :: [FilePath] -> [FilePath] -> Bool -> Config
+ Configuration.Dotenv: Config :: [FilePath] -> [FilePath] -> Bool -> Bool -> Bool -> Config
- Configuration.Dotenv: loadFile :: MonadIO m => Config -> m [(String, String)]
+ Configuration.Dotenv: loadFile :: MonadIO m => Config -> m ()
Files
- CHANGELOG.md +4/−0
- README.md +11/−8
- app/Main.hs +13/−4
- dotenv.cabal +1/−1
- spec/Configuration/DotenvSpec.hs +22/−11
- src/Configuration/Dotenv.hs +75/−39
- src/Configuration/Dotenv/Parse.hs +1/−1
- src/Configuration/Dotenv/Types.hs +31/−0
CHANGELOG.md view
@@ -1,4 +1,8 @@ ## MASTER+## Dotenv 0.10.0.0+### Modified+* `loadFile` change return type (back to `m ()`)+ ## Dotenv 0.9.0.3 ### Added * Parse multi-word command interpolations (Kudos to @pbrisbin)
README.md view
@@ -50,7 +50,7 @@ ### Variable substitution -In order to use compound env vars use the following sintax within your env vars+In order to use compound env vars use the following syntax within your env vars ${your_env_var}. For instance: ```@@ -66,8 +66,7 @@ ### Command substitution -In order to use the standard output of a command in your env vars use the following-sintax $(your_command). For instance:+In order to use the standard output of a command in your env vars use the following syntax $(your_command). For instance: ``` DATABASE=postgres://$(whoami)@localhost/database@@ -82,7 +81,7 @@ ### Configuration -The first argument to `loadFile` specifies the configuration. You cans use+The first argument to `loadFile` specifies the configuration. You can use `defaultConfig` which parses the `.env` file in your current directory and doesn't override your envs. You can also define your own configuration with the `Config` type.@@ -99,6 +98,11 @@ (e.g `[".env.example", ".tokens.example", ".public_keys.example"]`). If you don't need this functionality you can set `configExamplePath` to an empty list. +A `False` in the `configVerbose` means that Dotenv will not print any message+when loading the envs. A `True` means that Dotenv will print a message when a variable is loaded.++A `False` on `allowDuplicates` means that Dotenv will not allow duplicate keys, and instead it will throw+an error. A `True` means that Dotenv will allow duplicate keys, and it will use the first one if,`configOverride` is not set to `True`. ### Advanced Dotenv File Syntax You can add comments to your Dotenv file, on separate lines or after@@ -118,14 +122,13 @@ $ dotenv -f mydotenvfile myprogram ``` -The `-f` flag is optional, by default it looks for the `.env` file in the current-working directory.+The `-f` flag is optional, by default it looks for the `.env` file in the current working directory. ```shell $ dotenv myprogram ``` -Aditionally you can pass arguments and flags to the program passed to+Additionally, you can pass arguments and flags to the program passed to Dotenv: ```shell@@ -181,5 +184,5 @@ Do you want to contribute to this project? Please take a look at our [contributing guideline](/docs/CONTRIBUTING.md) to know how you can help us build it. ----<img src="https://www.stackbuilders.com/media/images/Sb-supports.original.png" alt="Stack Builders" width="50%"></img>+<img src="https://cdn.stackbuilders.com/media/images/Sb-supports.original.png" alt="Stack Builders" width="50%"></img> [Check out our libraries](https://github.com/stackbuilders/) | [Join our team](https://www.stackbuilders.com/join-us/)
app/Main.hs view
@@ -11,10 +11,7 @@ import Options.Applicative import Paths_dotenv (version) -import Control.Monad (void)- import Configuration.Dotenv (Config (..), defaultConfig, loadFile)- import System.Exit (exitWith) import System.Process (system) @@ -22,6 +19,8 @@ { dotenvFiles :: [String] , dotenvExampleFiles :: [String] , override :: Bool+ , verbose :: Bool+ , duplicates :: Bool , program :: String , args :: [String] } deriving (Show)@@ -33,13 +32,15 @@ Config { configExamplePath = dotenvExampleFiles , configOverride = override+ , configVerbose = verbose+ , allowDuplicates = duplicates , configPath = if null dotenvFiles then configPath defaultConfig else dotenvFiles } in do- void $ loadFile configDotenv+ loadFile configDotenv system (program ++ concatMap (" " ++) args) >>= exitWith where opts = info (helper <*> versionOption <*> config)@@ -67,6 +68,14 @@ <*> switch ( long "overload" <> short 'o' <> help "Specify this flag to override existing variables" )++ <*> switch ( long "verbose"+ <> help "Specify this flag to print out the variables loaded and other useful insights" )++ <*> flag True False ( long "no-dups"+ <> short 'D'+ <> help "Specify this flag to forbid duplicate variables"+ ) <*> argument str (metavar "PROGRAM")
dotenv.cabal view
@@ -1,5 +1,5 @@ name: dotenv-version: 0.9.0.3+version: 0.10.0.0 synopsis: Loads environment variables from dotenv files homepage: https://github.com/stackbuilders/dotenv-hs description:
spec/Configuration/DotenvSpec.hs view
@@ -12,7 +12,7 @@ import Test.Hspec -import Control.Monad (liftM, void)+import Control.Monad (liftM) import Data.Maybe (fromMaybe) import System.Process (readCreateProcess, shell) @@ -47,31 +47,30 @@ it "loads the configuration options to the environment from a file" $ do lookupEnv "DOTENV" `shouldReturn` Nothing - void $ loadFile $ Config ["spec/fixtures/.dotenv"] [] False+ loadFile sampleConfig lookupEnv "DOTENV" `shouldReturn` Just "true" it "respects predefined settings when overload is false" $ do setEnv "DOTENV" "preset" - void $ loadFile $ Config ["spec/fixtures/.dotenv"] [] False+ loadFile sampleConfig lookupEnv "DOTENV" `shouldReturn` Just "preset" it "overrides predefined settings when overload is true" $ do setEnv "DOTENV" "preset" - void $ loadFile $ Config ["spec/fixtures/.dotenv"] [] True+ loadFile sampleConfig { configOverride = True } lookupEnv "DOTENV" `shouldReturn` Just "true" context "when the .env.example is present" $ do- let config = Config ["spec/fixtures/.dotenv"] ["spec/fixtures/.dotenv.example"] False-+ let config = sampleConfig { configExamplePath = ["spec/fixtures/.dotenv.example"]} context "when the needed env vars are missing" $ it "should fail with an error call" $ do unsetEnv "ANOTHER_ENV"- void $ loadFile config `shouldThrow` anyErrorCall+ loadFile config `shouldThrow` anyErrorCall context "when the needed env vars are not missing" $ it "should succeed when loading all of the needed env vars" $ do@@ -80,7 +79,7 @@ home <- fromMaybe "" <$> lookupEnv "HOME" -- Load envs- void $ loadFile config+ loadFile config -- Check existing envs lookupEnv "ENVIRONMENT" `shouldReturn` Just home@@ -125,17 +124,29 @@ describe "onMissingFile" $ after_ clearEnvs $ do context "when target file is present" $ it "loading works as usual" $ do- void $ onMissingFile (loadFile $ Config ["spec/fixtures/.dotenv"] [] True) (return [])+ onMissingFile (loadFile sampleConfig {configOverride = True}) (return ()) lookupEnv "DOTENV" `shouldReturn` Just "true" context "when target file is missing" $ it "executes supplied handler instead" $- onMissingFile (True <$ loadFile (Config ["spec/fixtures/foo"] [] True)) (return False)+ onMissingFile (True <$ loadFile sampleConfig {configPath= ["spec/fixtures/.foo"], configOverride = True}) (return False) `shouldReturn` False + describe "onDuplicatedKeys" $ after_ clearEnvs $ do+ context "when duplicates are forbidden" $ do+ it "throws an error on duplicated keys" $+ loadFile sampleConfig { configPath = ["spec/fixtures/.dotenv", "spec/fixtures/.dotenv"], allowDuplicates = False }+ `shouldThrow` anyIOException+ it "works as usual when there is no duplicated keys" $ do+ loadFile sampleConfig { allowDuplicates = False }+ lookupEnv "DOTENV" `shouldReturn` Just "true"++sampleConfig :: Config+sampleConfig = Config ["spec/fixtures/.dotenv"] [] False False True+ clearEnvs :: IO () clearEnvs =- fmap (fmap fst) getEnvironment >>= mapM_ unsetEnv+ getEnvironment >>= mapM_ unsetEnv . fmap fst setupEnv :: IO () setupEnv = do
src/Configuration/Dotenv.hs view
@@ -8,100 +8,136 @@ -- Portability : portable -- -- This module contains common functions to load and read dotenv files.- {-# LANGUAGE RecordWildCards #-} module Configuration.Dotenv- (- -- * Dotenv Load Functions+ ( -- * Dotenv Load Functions load , loadFile , parseFile , onMissingFile- -- * Dotenv Types+ -- * Dotenv Types , module Configuration.Dotenv.Types- )- where+ ) where import Configuration.Dotenv.Environment (getEnvironment, lookupEnv, setEnv) import Configuration.Dotenv.Parse (configParser) import Configuration.Dotenv.ParsedVariable (interpolateParsedVariables)-import Configuration.Dotenv.Types (Config (..),- defaultConfig)-import Control.Monad (liftM)+import Configuration.Dotenv.Types (Config (..), ReaderT, ask,+ defaultConfig,+ liftReaderT, runReaderT)+import Control.Exception (throw)+import Control.Monad (when) import Control.Monad.Catch+import Control.Monad.Compat (unless) import Control.Monad.IO.Class (MonadIO (..)) import Data.List (intersectBy, union, unionBy) import System.IO.Error (isDoesNotExistError) import Text.Megaparsec (errorBundlePretty, parse) +-- | Monad Stack for the application+type DotEnv m a = ReaderT Config m a+ -- | Loads the given list of options into the environment. Optionally -- override existing variables with values from Dotenv files. load ::- MonadIO m =>- Bool -- ^ Override existing settings?+ MonadIO m+ => Bool -- ^ Override existing settings? -> [(String, String)] -- ^ List of values to be set in environment -> m ()-load override = mapM_ (applySetting override)+load override kv =+ runReaderT (mapM_ applySetting kv) defaultConfig {configOverride = override} -- | @loadFile@ parses the environment variables defined in the dotenv example -- file and checks if they are defined in the dotenv file or in the environment. -- It also allows to override the environment variables defined in the environment -- with the values defined in the dotenv file.-loadFile- :: MonadIO m+loadFile ::+ MonadIO m => Config -- ^ Dotenv configuration- -> m [(String, String)] -- ^ Environment variables loaded-loadFile Config{..} = do+ -> m ()+loadFile config@Config {..} = do environment <- liftIO getEnvironment- readedVars <- concat `liftM` mapM parseFile configPath- neededVars <- concat `liftM` mapM parseFile configExamplePath- let coincidences = (environment `union` readedVars) `intersectEnvs` neededVars+ readVars <- fmap concat (mapM parseFile configPath)+ neededVars <- fmap concat (mapM parseFile configExamplePath)+ let coincidences = (environment `union` readVars) `intersectEnvs` neededVars cmpEnvs env1 env2 = fst env1 == fst env2 intersectEnvs = intersectBy cmpEnvs unionEnvs = unionBy cmpEnvs vars = if (not . null) neededVars- then- if length neededVars == length coincidences- then readedVars `unionEnvs` neededVars- else error $ "Missing env vars! Please, check (this/these) var(s) (is/are) set:" ++ concatMap ((++) " " . fst) neededVars- else readedVars- mapM (applySetting configOverride) vars+ then if length neededVars == length coincidences+ then readVars `unionEnvs` neededVars+ else error $+ "Missing env vars! Please, check (this/these) var(s) (is/are) set:" +++ concatMap ((++) " " . fst) neededVars+ else readVars+ unless allowDuplicates $ (lookUpDuplicates . map fst) vars+ runReaderT (mapM_ applySetting vars) config -- | Parses the given dotenv file and returns values /without/ adding them to -- the environment. parseFile ::- MonadIO m =>- FilePath -- ^ A file containing options to read+ MonadIO m+ => FilePath -- ^ A file containing options to read -> m [(String, String)] -- ^ Variables contained in the file parseFile f = do contents <- liftIO $ readFile f- case parse configParser f contents of Left e -> error $ errorBundlePretty e Right options -> liftIO $ interpolateParsedVariables options -applySetting :: MonadIO m => Bool -> (String, String) -> m (String, String)-applySetting override (key, value) =- if override- then liftIO (setEnv key value) >> return (key, value)+applySetting ::+ MonadIO m+ => (String, String) -- ^ A key-value pair to set in the environment+ -> DotEnv m (String, String)+applySetting kv@(k, v) = do+ Config {..} <- ask+ if configOverride+ then info kv >> setEnv' else do- res <- liftIO $ lookupEnv key-+ res <- liftReaderT . liftIO $ lookupEnv k case res of- Nothing -> liftIO $ setEnv key value >> return (key, value)- Just _ -> return (key, value)+ Nothing -> info kv >> setEnv'+ Just _ -> return kv+ where+ setEnv' = liftReaderT . liftIO $ setEnv k v >> return kv +-- | The function logs in console when a variable is loaded into the+-- environment.+info :: MonadIO m => (String, String) -> DotEnv m ()+info (key, value) = do+ Config {..} <- ask+ when configVerbose $+ liftReaderT . liftIO $+ putStrLn $ "[INFO]: Load env '" ++ key ++ "' with value '" ++ value ++ "'"+ -- | The helper allows to avoid exceptions in the case of missing files and -- perform some action instead. -- -- @since 0.3.1.0--onMissingFile :: MonadCatch m+onMissingFile ::+ MonadCatch m => m a -- ^ Action to perform that may fail because of missing file- -> m a -- ^ Action to perform if file is indeed missing+ -> m a -- ^ Action to perform if file is indeed missing -> m a onMissingFile f h = catchIf isDoesNotExistError f (const h)++-- | The helper throws an exception if the allow duplicate is set to False.+forbidDuplicates :: MonadIO m => String -> m ()+forbidDuplicates key =+ throw $+ userError $+ "[ERROR]: Env '" +++ key +++ "' is duplicated in a dotenv file. Please, fix that (or remove --no-dups)."++lookUpDuplicates :: MonadIO m => [String] -> m ()+lookUpDuplicates [] = return ()+lookUpDuplicates [_] = return ()+lookUpDuplicates (x:xs) =+ if x `elem` xs+ then forbidDuplicates x+ else lookUpDuplicates xs
src/Configuration/Dotenv/Parse.hs view
@@ -70,7 +70,7 @@ interpolatedValueVarInterpolation :: Parser VarFragment interpolatedValueVarInterpolation = VarInterpolation <$>- ((between (symbol "${") (symbol "}") variableName) <|>+ (between (symbol "${") (symbol "}") variableName <|> (char '$' >> variableName)) where symbol = L.symbol sc
src/Configuration/Dotenv/Types.hs view
@@ -12,6 +12,10 @@ module Configuration.Dotenv.Types ( Config(..) , defaultConfig+ , ask+ , runReaderT+ , liftReaderT+ , ReaderT ) where @@ -20,6 +24,8 @@ { configPath :: [FilePath] -- ^ The paths for the .env files , configExamplePath :: [FilePath] -- ^ The paths for the .env.example files , configOverride :: Bool -- ^ Flag to allow override env variables+ , configVerbose :: Bool -- ^ Flag to log the loaded variables and other useful information+ , allowDuplicates :: Bool -- ^ Flag to allow duplicate variables } deriving (Eq, Show) -- | Default configuration. Use .env file without .env.example strict envs and@@ -30,4 +36,29 @@ { configExamplePath = [] , configOverride = False , configPath = [ ".env" ]+ , configVerbose = False+ , allowDuplicates = True }+++newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }+mapReaderT :: (m a -> n b) -> ReaderT r m a -> ReaderT r n b+mapReaderT f m = ReaderT $ f . runReaderT m+instance (Functor m) => Functor (ReaderT r m) where+ fmap f = mapReaderT (fmap f)+instance (Applicative m) => Applicative (ReaderT r m) where+ pure = liftReaderT . pure+ f <*> v = ReaderT $ \ r -> runReaderT f r <*> runReaderT v r++instance (Monad m) => Monad (ReaderT r m) where+ return = liftReaderT . return+ m >>= k = ReaderT $ \ r -> do+ a <- runReaderT m r+ runReaderT (k a) r+ m >> k = ReaderT $ \ r -> runReaderT m r >> runReaderT k r++liftReaderT :: m a -> ReaderT r m a+liftReaderT m = ReaderT (const m)++ask :: (Monad m) => ReaderT r m r+ask = ReaderT return