Skip to content

Commit 0e08424

Browse files
Add dotenv-safe (#57)
* Add dotenv-safe * Add Dotenv.Types to cabal file * Update README and CHANGELOG * Follow suggestions: * Remove -x flag in dotenv executable * Update README and CHANGELOG * Improve test description in DotenvSpec
1 parent cd34912 commit 0e08424

File tree

8 files changed

+172
-46
lines changed

8 files changed

+172
-46
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
## MASTER
2+
## Dotenv 0.5.0.0
3+
4+
* Add [dotenv-safe functionality](https://www.npmjs.com/package/dotenv-safe)
5+
* Add the `Config` type with options to override env variables, and setting the
6+
path for .env and .env.example files.
7+
* Changed `loadFile` function to get `Config` with the paths for the .env file
8+
and the .env.example file.
29

310
## Dotenv 0.4.0.0
411

README.md

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,50 @@ place to understand the nuances of Dotenv file parsing.
8181
You can call dotenv from the command line in order to load settings
8282
from one or more dotenv file before invoking an executable:
8383

84-
```
85-
dotenv -f mydotenvfile myprogram
84+
```shell
85+
$ dotenv -e mydotenvfile myprogram
8686
```
8787

8888
Aditionally you can pass arguments and flags to the program passed to
8989
Dotenv:
9090

91+
```shell
92+
$ dotenv -e mydotenvfile myprogram -- --myflag myargument
93+
```
94+
95+
or:
96+
97+
```shell
98+
$ dotenv -e mydotenvfile "myprogram --myflag myargument"
99+
```
100+
101+
Also, you can use a `--example` flag to use [dotenv-safe functionality](https://www.npmjs.com/package/dotenv-safe)
102+
so that you can have a list of strict envs that should be defined in the environment
103+
or in your dotenv files before the execution of your program. For instance:
104+
105+
```shell
106+
$ cat .env.example
107+
DOTENV=
108+
FOO=
109+
BAR=
110+
111+
$ cat .env
112+
DOTENV=123
113+
114+
$ echo $FOO
115+
123
91116
```
92-
dotenv -f mydotenvfile myprogram -- --myflag myargument
117+
118+
This will fail:
119+
```shell
120+
$ dotenv -e .env --example .env.example "myprogram --myflag myargument"
121+
> dotenv: Missing env vars! Please, check (this/these) var(s) (is/are) set: BAR
122+
```
123+
124+
This will succeed:
125+
```shell
126+
$ export BAR=123 # Or you can do something like: "echo 'BAR=123' >> .env"
127+
$ dotenv -e .env --example .env.example "myprogram --myflag myargument"
93128
```
94129

95130
Hint: The `env` program in most Unix-like environments prints out the

app/Main.hs

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
{-# LANGUAGE CPP #-}
1+
{-# LANGUAGE CPP #-}
2+
{-# LANGUAGE RecordWildCards #-}
23

34
module Main where
45

@@ -8,35 +9,49 @@ import Data.Monoid ((<>))
89

910
import Options.Applicative
1011

11-
import Configuration.Dotenv (loadFile)
12+
import Control.Monad (void)
1213

13-
import Control.Monad.IO.Class(MonadIO(..))
14+
import Configuration.Dotenv (loadFile)
15+
import Configuration.Dotenv.Types (Config(..))
1416

1517
import System.Process (system)
1618
import System.Exit (exitWith)
1719

1820
data Options = Options
19-
{ files :: [String]
20-
, overload :: Bool
21-
, program :: String
22-
, args :: [String]
21+
{ dotenvFiles :: [String]
22+
, dotenvExampleFiles :: [String]
23+
, override :: Bool
24+
, program :: String
25+
, args :: [String]
2326
} deriving (Show)
2427

2528
main :: IO ()
26-
main = execParser opts >>= dotEnv
27-
where
28-
opts = info (helper <*> config)
29-
( fullDesc
30-
<> progDesc "Runs PROGRAM after loading options from FILE"
31-
<> header "dotenv - loads options from dotenv files" )
29+
main = do
30+
Options{..} <- execParser opts
31+
void $ loadFile Config
32+
{ configExamplePath = dotenvExampleFiles
33+
, configOverride = override
34+
, configPath = dotenvFiles
35+
}
36+
system (program ++ concatMap (" " ++) args) >>= exitWith
37+
where
38+
opts = info (helper <*> config)
39+
( fullDesc
40+
<> progDesc "Runs PROGRAM after loading options from FILE"
41+
<> header "dotenv - loads options from dotenv files" )
3242

3343
config :: Parser Options
3444
config = Options
3545
<$> some (strOption (
36-
long "file"
46+
long "dotenv"
3747
<> short 'f'
38-
<> metavar "FILE"
39-
<> help "File to read for options" ))
48+
<> metavar "DOTENV"
49+
<> help "File to read for environmental variables" ))
50+
51+
<*> many (strOption (
52+
long "example"
53+
<> metavar "DOTENV_EXAMPLE"
54+
<> help "File to read for needed environmental variables" ))
4055

4156
<*> switch ( long "overload"
4257
<> short 'o'
@@ -46,10 +61,3 @@ config = Options
4661

4762
<*> many (argument str (metavar "ARG"))
4863

49-
dotEnv :: MonadIO m => Options -> m ()
50-
dotEnv opts = liftIO $ do
51-
mapM_ (loadFile (overload opts)) (files opts)
52-
code <- system (program opts ++ programArguments)
53-
exitWith code
54-
where
55-
programArguments = concatMap (" " ++) (args opts)

dotenv.cabal

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: dotenv
2-
version: 0.4.0.0
2+
version: 0.5.0.0
33
synopsis: Loads environment variables from dotenv files
44
homepage: https://github.com/stackbuilders/dotenv-hs
55
description:
@@ -72,6 +72,7 @@ library
7272
, Configuration.Dotenv.Parse
7373
, Configuration.Dotenv.ParsedVariable
7474
, Configuration.Dotenv.Text
75+
, Configuration.Dotenv.Types
7576

7677
build-depends: base >=4.7 && <5.0
7778
, base-compat >= 0.4
@@ -100,6 +101,7 @@ test-suite dotenv-test
100101
, Configuration.Dotenv.ParseSpec
101102
, Configuration.Dotenv
102103
, Configuration.Dotenv.Text
104+
, Configuration.Dotenv.Types
103105
, Configuration.Dotenv.Parse
104106
, Configuration.Dotenv.ParsedVariable
105107

spec/Configuration/DotenvSpec.hs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
module Configuration.DotenvSpec (main, spec) where
44

5+
import Configuration.Dotenv.Types (Config(..))
56
import Configuration.Dotenv (load, loadFile, parseFile, onMissingFile)
67

78
import Test.Hspec
@@ -52,33 +53,48 @@ spec = do
5253

5354
lookupEnv "foo" `shouldReturn` Just "new setting"
5455

55-
describe "loadFile" $ after_ (unsetEnv "DOTENV") $ do
56+
describe "loadFile" $ after_ (mapM_ unsetEnv ["DOTENV" , "ANOTHER_ENV"]) $ do
5657
it "loads the configuration options to the environment from a file" $ do
5758
lookupEnv "DOTENV" `shouldReturn` Nothing
5859

59-
loadFile False "spec/fixtures/.dotenv"
60+
loadFile $ Config ["spec/fixtures/.dotenv"] [] False
6061

6162
lookupEnv "DOTENV" `shouldReturn` Just "true"
6263

6364
it "respects predefined settings when overload is false" $ do
6465
setEnv "DOTENV" "preset"
6566

66-
loadFile False "spec/fixtures/.dotenv"
67+
loadFile $ Config ["spec/fixtures/.dotenv"] [] False
6768

6869
lookupEnv "DOTENV" `shouldReturn` Just "preset"
6970

7071
it "overrides predefined settings when overload is true" $ do
7172
setEnv "DOTENV" "preset"
7273

73-
loadFile True "spec/fixtures/.dotenv"
74+
loadFile $ Config ["spec/fixtures/.dotenv"] [] True
7475

7576
lookupEnv "DOTENV" `shouldReturn` Just "true"
7677

78+
context "when the .env.example is present" $ do
79+
let config = Config ["spec/fixtures/.dotenv"] ["spec/fixtures/.dotenv.example"] False
80+
81+
context "when the needed env vars are missing" $
82+
it "should fail with an error call" $
83+
loadFile config `shouldThrow` anyErrorCall
84+
85+
context "when the needed env vars are not missing" $
86+
it "should succeed when loading all of the needed env vars" $ do
87+
setEnv "ANOTHER_ENV" "hello"
88+
loadFile config `shouldReturn` ()
89+
lookupEnv "DOTENV" `shouldReturn` Just "true"
90+
lookupEnv "UNICODE_TEST" `shouldReturn` Just "Manabí"
91+
lookupEnv "ANOTHER_ENV" `shouldReturn` Just "hello"
92+
7793
describe "parseFile" $ after_ (unsetEnv "DOTENV") $ do
7894
it "returns variables from a file without changing the environment" $ do
7995
lookupEnv "DOTENV" `shouldReturn` Nothing
8096

81-
(liftM head $ parseFile "spec/fixtures/.dotenv") `shouldReturn`
97+
liftM head (parseFile "spec/fixtures/.dotenv") `shouldReturn`
8298
("DOTENV", "true")
8399

84100
lookupEnv "DOTENV" `shouldReturn` Nothing
@@ -99,9 +115,10 @@ spec = do
99115
describe "onMissingFile" $ after_ (unsetEnv "DOTENV") $ do
100116
context "when target file is present" $
101117
it "loading works as usual" $ do
102-
onMissingFile (loadFile True "spec/fixtures/.dotenv") (return ())
118+
onMissingFile (loadFile $ Config ["spec/fixtures/.dotenv"] [] True) (return ())
103119
lookupEnv "DOTENV" `shouldReturn` Just "true"
120+
104121
context "when target file is missing" $
105122
it "executes supplied handler instead" $
106-
onMissingFile (True <$ loadFile True "spec/fixtures/foo") (return False)
123+
onMissingFile (True <$ (loadFile $ Config ["spec/fixtures/foo"] [] True)) (return False)
107124
`shouldReturn` False

spec/fixtures/.dotenv.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
DOTENV=
2+
UNICODE_TEST=
3+
ANOTHER_ENV=

src/Configuration/Dotenv.hs

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
--
1010
-- This module contains common functions to load and read dotenv files.
1111

12-
{-# LANGUAGE CPP #-}
12+
{-# LANGUAGE CPP #-}
13+
{-# LANGUAGE RecordWildCards #-}
1314

1415
module Configuration.Dotenv
1516
( load
@@ -18,18 +19,21 @@ module Configuration.Dotenv
1819
, onMissingFile )
1920
where
2021

22+
import Control.Monad (liftM)
2123
import Configuration.Dotenv.Parse (configParser)
2224
import Configuration.Dotenv.ParsedVariable (interpolateParsedVariables)
25+
import Configuration.Dotenv.Types (Config(..))
2326
import Control.Monad.Catch
2427
import Control.Monad.IO.Class (MonadIO(..))
28+
import Data.List (union, intersectBy, unionBy)
2529
import System.Environment (lookupEnv)
2630
import System.IO.Error (isDoesNotExistError)
27-
import Text.Megaparsec (parse)
31+
import Text.Megaparsec (parse, parseErrorPretty)
2832

2933
#if MIN_VERSION_base(4,7,0)
30-
import System.Environment (setEnv)
34+
import System.Environment (getEnvironment, setEnv)
3135
#else
32-
import System.Environment.Compat (setEnv)
36+
import System.Environment.Compat (getEnvironment, setEnv)
3337
#endif
3438

3539
-- | Loads the given list of options into the environment. Optionally
@@ -41,14 +45,30 @@ load ::
4145
-> m ()
4246
load override = mapM_ (applySetting override)
4347

44-
-- | Loads the options in the given file to the environment. Optionally
45-
-- override existing variables with values from Dotenv files.
46-
loadFile ::
47-
MonadIO m =>
48-
Bool -- ^ Override existing settings?
49-
-> FilePath -- ^ A file containing options to load into the environment
48+
-- | @loadFile@ parses the environment variables defined in the dotenv example
49+
-- file and checks if they are defined in the dotenv file or in the environment.
50+
-- It also allows to override the environment variables defined in the environment
51+
-- with the values defined in the dotenv file.
52+
loadFile
53+
:: MonadIO m
54+
=> Config
5055
-> m ()
51-
loadFile override f = load override =<< parseFile f
56+
loadFile Config{..} = do
57+
environment <- liftIO getEnvironment
58+
readedVars <- concat `liftM` mapM parseFile configPath
59+
neededVars <- concat `liftM` mapM parseFile configExamplePath
60+
let coincidences = (environment `union` readedVars) `intersectEnvs` neededVars
61+
cmpEnvs env1 env2 = fst env1 == fst env2
62+
intersectEnvs = intersectBy cmpEnvs
63+
unionEnvs = unionBy cmpEnvs
64+
vars =
65+
if (not . null) neededVars
66+
then
67+
if length neededVars == length coincidences
68+
then readedVars `unionEnvs` neededVars
69+
else error $ "Missing env vars! Please, check (this/these) var(s) (is/are) set:" ++ concatMap ((++) " " . fst) neededVars
70+
else readedVars
71+
mapM_ (applySetting configOverride) vars
5272

5373
-- | Parses the given dotenv file and returns values /without/ adding them to
5474
-- the environment.
@@ -60,7 +80,7 @@ parseFile f = do
6080
contents <- liftIO $ readFile f
6181

6282
case parse configParser f contents of
63-
Left e -> error $ "Failed to read file" ++ show e
83+
Left e -> error $ parseErrorPretty e
6484
Right options -> liftIO $ interpolateParsedVariables options
6585

6686
applySetting :: MonadIO m => Bool -> (String, String) -> m ()

src/Configuration/Dotenv/Types.hs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{- |
2+
-- Module : Configuration.Dotenv.Types
3+
-- Copyright : © 2015–2017 Stack Builders Inc.
4+
-- License : MIT
5+
--
6+
-- Maintainer : Stack Builders <hackage@stackbuilders.com>
7+
-- Stability : experimental
8+
-- Portability : portable
9+
--
10+
-- Provides the types with extra options for loading a dotenv file.
11+
-}
12+
13+
module Configuration.Dotenv.Types
14+
( Config(..)
15+
, defaultConfig
16+
)
17+
where
18+
19+
-- | Configuration Data Types with extra options for executing dotenv.
20+
data Config = Config
21+
{ configPath :: [FilePath] -- ^ The paths for the .env files
22+
, configExamplePath :: [FilePath] -- ^ The paths for the .env.example files
23+
, configOverride :: Bool -- ^ Flag to allow override env variables
24+
} deriving (Eq, Show)
25+
26+
-- | Default configuration. Use .env file without .env.example strict envs and
27+
-- without overriding.
28+
defaultConfig :: Config
29+
defaultConfig =
30+
Config
31+
{ configExamplePath = []
32+
, configOverride = False
33+
, configPath = [ ".env" ]
34+
}

0 commit comments

Comments
 (0)