dotenv 0.3.0.3 → 0.3.1.0
raw patch · 9 files changed
+218/−127 lines, 9 filesdep +exceptionsdep +hspec-megaparsecdep +transformersdep ~basedep ~dotenvnew-uploaderPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: exceptions, hspec-megaparsec, transformers
Dependency ranges changed: base, dotenv
API changes (from Hackage documentation)
+ Configuration.Dotenv: onMissingFile :: MonadCatch m => m a -> m a -> m a
- Configuration.Dotenv: load :: Bool -> [(String, String)] -> IO ()
+ Configuration.Dotenv: load :: MonadIO m => Bool -> [(String, String)] -> m ()
- Configuration.Dotenv: loadFile :: Bool -> FilePath -> IO ()
+ Configuration.Dotenv: loadFile :: MonadIO m => Bool -> FilePath -> m ()
- Configuration.Dotenv: parseFile :: FilePath -> IO [(String, String)]
+ Configuration.Dotenv: parseFile :: MonadIO m => FilePath -> m [(String, String)]
- Configuration.Dotenv.Text: parseFile :: FilePath -> IO [(Text, Text)]
+ Configuration.Dotenv.Text: parseFile :: MonadIO m => FilePath -> m [(Text, Text)]
Files
- CHANGELOG.md +14/−1
- app/Main.hs +5/−3
- dotenv.cabal +30/−8
- spec/Configuration/Dotenv/ParseSpec.hs +33/−39
- spec/Configuration/Dotenv/TextSpec.hs +9/−1
- spec/Configuration/DotenvSpec.hs +24/−2
- src/Configuration/Dotenv.hs +39/−11
- src/Configuration/Dotenv/Parse.hs +57/−60
- src/Configuration/Dotenv/Text.hs +7/−2
CHANGELOG.md view
@@ -1,3 +1,16 @@+## Dotenv 0.3.1.0++* Made interface more polymorphic so the functions works in any instance of+ `MonadIO`, not only `IO`. This should reduce amount of lifting in some+ cases.++* Added `onMissingFile` helper to deal with possibly missing files.++* Parser was rewritten to take full advantage of Megaparsec.+ `hspec-megaparsec` is now used for testing of the parser.++* Dropped support for GHC 7.4.+ ## Dotenv 0.3.0.3 * Allow optparse-applicative 0.13@@ -20,7 +33,7 @@ * Added function `parseFile` to read dotenv file without modifying the environment. Thanks to Daisuke Fujimura (Github: fujimura) for making this contribution.- + ## Dotenv 0.1.0.0 * First public release.
app/Main.hs view
@@ -10,6 +10,8 @@ import Configuration.Dotenv (loadFile) +import Control.Monad.IO.Class(MonadIO(..))+ import System.Process (system) import System.Exit (exitWith) @@ -41,8 +43,8 @@ <> short 'o' <> help "Specify this flag to override existing variables" ) -dotEnv :: Options -> IO ()-dotEnv opts = do+dotEnv :: MonadIO m => Options -> m ()+dotEnv opts = liftIO $ do mapM_ (loadFile (overload opts)) (files opts)- code <- system $ program opts+ code <- system (program opts) exitWith code
dotenv.cabal view
@@ -1,5 +1,5 @@ name: dotenv-version: 0.3.0.3+version: 0.3.1.0 synopsis: Loads environment variables from dotenv files homepage: https://github.com/stackbuilders/dotenv-hs description:@@ -44,17 +44,27 @@ cabal-version: >=1.10 bug-reports: https://github.com/stackbuilders/dotenv-hs/issues +flag dev+ description: Turn on development settings.+ manual: True+ default: False+ executable dotenv main-is: Main.hs- build-depends: base >=4.5 && <5.0+ build-depends: base >=4.6 && <5.0 , base-compat >= 0.4- , dotenv >= 0.3.0.2+ , dotenv >= 0.3.1.0 , optparse-applicative >=0.11 && < 0.14 , megaparsec >= 5.0 && < 6.0 , process , text+ , transformers >=0.4 && < 0.6 hs-source-dirs: app+ if flag(dev)+ ghc-options: -Wall -Werror+ else+ ghc-options: -O2 -Wall default-language: Haskell2010 library@@ -62,14 +72,20 @@ , Configuration.Dotenv.Parse , Configuration.Dotenv.Text - build-depends: base >=4.5 && <5.0+ build-depends: base >=4.6 && <5.0 , base-compat >= 0.4 , megaparsec >= 5.0 && < 6.0 , text+ , transformers >=0.4 && < 0.6+ , exceptions >= 0.8 && < 0.9 hs-source-dirs: src- default-language: Haskell2010 ghc-options: -Wall+ if flag(dev)+ ghc-options: -Wall -Werror+ else+ ghc-options: -O2 -Wall+ default-language: Haskell2010 test-suite dotenv-test type: exitcode-stdio-1.0@@ -82,15 +98,21 @@ , Configuration.Dotenv.Text , Configuration.Dotenv.Parse - build-depends: base >=4.5 && <5.0+ build-depends: base >=4.6 && <5.0 , base-compat >= 0.4- , dotenv >= 0.3.0.2+ , dotenv >= 0.3.1.0 , megaparsec >= 5.0 && < 6.0 , hspec , text+ , transformers >=0.4 && < 0.6+ , exceptions >= 0.8 && < 0.9+ , hspec-megaparsec >= 0.2 && < 0.4 + if flag(dev)+ ghc-options: -Wall -Werror+ else+ ghc-options: -O2 -Wall default-language: Haskell2010- ghc-options: -Wall source-repository head type: git
spec/Configuration/Dotenv/ParseSpec.hs view
@@ -3,9 +3,8 @@ module Configuration.Dotenv.ParseSpec (main, spec) where import Configuration.Dotenv.Parse (configParser)--import Test.Hspec (it, describe, shouldBe, Spec, hspec)-+import Test.Hspec (it, describe, Spec, hspec)+import Test.Hspec.Megaparsec (shouldParse, shouldFailOn) import Text.Megaparsec (ParseError, Dec, parse) main :: IO ()@@ -14,81 +13,76 @@ spec :: Spec spec = describe "parse" $ do it "parses unquoted values" $- parseConfig "FOO=bar" `shouldBe` Right [("FOO", "bar")]+ parseConfig "FOO=bar" `shouldParse` [("FOO", "bar")] it "parses values with spaces around equal signs" $ do- parseConfig "FOO =bar" `shouldBe` Right [("FOO", "bar")]- parseConfig "FOO= bar" `shouldBe` Right [("FOO", "bar")]- parseConfig "FOO =\t bar" `shouldBe` Right [("FOO", "bar")]+ parseConfig "FOO =bar" `shouldParse` [("FOO", "bar")]+ parseConfig "FOO= bar" `shouldParse` [("FOO", "bar")]+ parseConfig "FOO =\t bar" `shouldParse` [("FOO", "bar")] it "parses double-quoted values" $- parseConfig "FOO=\"bar\"" `shouldBe` Right [("FOO", "bar")]+ parseConfig "FOO=\"bar\"" `shouldParse` [("FOO", "bar")] it "parses single-quoted values" $- parseConfig "FOO='bar'" `shouldBe` Right [("FOO", "bar")]+ parseConfig "FOO='bar'" `shouldParse` [("FOO", "bar")] it "parses escaped double quotes" $- parseConfig "FOO=\"escaped\\\"bar\"" `shouldBe`- Right [("FOO", "escaped\"bar")]+ parseConfig "FOO=\"escaped\\\"bar\""+ `shouldParse` [("FOO", "escaped\"bar")] it "supports CRLF line breaks" $- parseConfig "FOO=bar\r\nbaz=fbb" `shouldBe`- Right [("FOO", "bar"), ("baz", "fbb")]+ parseConfig "FOO=bar\r\nbaz=fbb"+ `shouldParse` [("FOO", "bar"), ("baz", "fbb")] it "parses empty values" $- parseConfig "FOO=" `shouldBe` Right [("FOO", "")]+ parseConfig "FOO=" `shouldParse` [("FOO", "")] it "does not parse if line format is incorrect" $ do- isLeft (parseConfig "lol$wut") `shouldBe` True- isLeft (parseConfig "KEY=\nVALUE") `shouldBe` True- isLeft (parseConfig "KEY\n=VALUE") `shouldBe` True+ parseConfig `shouldFailOn` "lol$wut"+ parseConfig `shouldFailOn` "KEY=\nVALUE"+ parseConfig `shouldFailOn` "KEY\n=VALUE" it "expands newlines in quoted strings" $- parseConfig "FOO=\"bar\nbaz\"" `shouldBe` Right [("FOO", "bar\nbaz")]+ parseConfig "FOO=\"bar\nbaz\"" `shouldParse` [("FOO", "bar\nbaz")] it "does not parse variables with hyphens in the name" $- isLeft (parseConfig "FOO-BAR=foobar") `shouldBe` True+ parseConfig `shouldFailOn` "FOO-BAR=foobar" it "parses variables with \"_\" in the name" $- parseConfig "FOO_BAR=foobar" `shouldBe` Right [("FOO_BAR", "foobar")]+ parseConfig "FOO_BAR=foobar" `shouldParse` [("FOO_BAR", "foobar")] it "parses variables with digits after the first character" $- parseConfig "FOO_BAR_12=foobar" `shouldBe` Right [("FOO_BAR_12", "foobar")]+ parseConfig "FOO_BAR_12=foobar" `shouldParse` [("FOO_BAR_12", "foobar")] it "allows vertical spaces after a quoted variable" $- parseConfig "foo='bar' " `shouldBe` Right [("foo", "bar")]+ parseConfig "foo='bar' " `shouldParse` [("foo", "bar")] it "does not parse variable names beginning with a digit" $- isLeft (parse configParser "null" "45FOO_BAR=foobar") `shouldBe` True+ parseConfig `shouldFailOn` "45FOO_BAR=foobar" it "strips unquoted values" $- parseConfig "foo=bar " `shouldBe` Right [("foo", "bar")]+ parseConfig "foo=bar " `shouldParse` [("foo", "bar")] it "ignores empty lines" $- parseConfig "\n \t \nfoo=bar\n \nfizz=buzz" `shouldBe`- Right [("foo", "bar"), ("fizz", "buzz")]+ parseConfig "\n \t \nfoo=bar\n \nfizz=buzz"+ `shouldParse` [("foo", "bar"), ("fizz", "buzz")] it "ignores inline comments after unquoted arguments" $- parseConfig "FOO=bar # this is foo" `shouldBe` Right [("FOO", "bar")]+ parseConfig "FOO=bar # this is foo" `shouldParse` [("FOO", "bar")] it "ignores inline comments after quoted arguments" $- parseConfig "FOO=\"bar\" # this is foo" `shouldBe` Right [("FOO", "bar")]+ parseConfig "FOO=\"bar\" # this is foo" `shouldParse` [("FOO", "bar")] it "allows \"#\" in quoted values" $- parseConfig "foo=\"bar#baz\" # comment" `shouldBe`- Right [("foo", "bar#baz")]+ parseConfig "foo=\"bar#baz\" # comment"+ `shouldParse` [("foo", "bar#baz")] it "ignores comment lines" $- parseConfig "\n\t \n\n # HERE GOES FOO \nfoo=bar" `shouldBe`- Right [("foo", "bar")]+ parseConfig "\n\t \n\n # HERE GOES FOO \nfoo=bar"+ `shouldParse` [("foo", "bar")] it "doesn't allow more configuration options after a quoted value" $- isLeft (parse configParser "null" "foo='bar'baz='buz'") `shouldBe` True---isLeft :: Either a b -> Bool-isLeft ( Left _ ) = True-isLeft _ = False+ parseConfig `shouldFailOn` "foo='bar'baz='buz'" parseConfig :: String -> Either (ParseError Char Dec) [(String, String)]-parseConfig = parse configParser "(unknown)"+parseConfig = parse configParser ""
spec/Configuration/Dotenv/TextSpec.hs view
@@ -1,12 +1,20 @@+{-# LANGUAGE CPP #-}+ module Configuration.Dotenv.TextSpec (main, spec) where import Configuration.Dotenv.Text (parseFile) import Test.Hspec -import System.Environment.Compat (lookupEnv, unsetEnv) import Control.Monad (liftM)+import System.Environment (lookupEnv) import qualified Data.Text as T++#if MIN_VERSION_base(4,7,0)+import System.Environment (unsetEnv)+#else+import System.Environment.Compat (unsetEnv)+#endif {-# ANN module ("HLint: ignore Reduce duplication" :: String) #-}
spec/Configuration/DotenvSpec.hs view
@@ -1,12 +1,24 @@+{-# LANGUAGE CPP #-}+ module Configuration.DotenvSpec (main, spec) where -import Configuration.Dotenv (load, loadFile, parseFile)+import Configuration.Dotenv (load, loadFile, parseFile, onMissingFile) import Test.Hspec -import System.Environment.Compat (lookupEnv, setEnv, unsetEnv)+import System.Environment (lookupEnv) import Control.Monad (liftM) +#if !MIN_VERSION_base(4,8,0)+import Control.Applicative ((<$))+#endif++#if MIN_VERSION_base(4,7,0)+import System.Environment (setEnv, unsetEnv)+#else+import System.Environment.Compat (setEnv, unsetEnv)+#endif+ {-# ANN module "HLint: ignore Reduce duplication" #-} main :: IO ()@@ -70,3 +82,13 @@ it "recognizes unicode characters" $ liftM (!! 1) (parseFile "spec/fixtures/.dotenv") `shouldReturn` ("UNICODE_TEST", "Manabí")++ describe "onMissingFile" $ after_ (unsetEnv "DOTENV") $ do+ context "when target file is present" $+ it "loading works as usual" $ do+ onMissingFile (loadFile True "spec/fixtures/.dotenv") (return ())+ lookupEnv "DOTENV" `shouldReturn` Just "true"+ context "when target file is missing" $+ it "executes supplied handler instead" $+ onMissingFile (True <$ loadFile True "spec/fixtures/foo") (return False)+ `shouldReturn` False
src/Configuration/Dotenv.hs view
@@ -9,50 +9,78 @@ -- -- This module contains common functions to load and read dotenv files. -module Configuration.Dotenv (load, loadFile, parseFile) where+{-# LANGUAGE CPP #-} -import System.Environment.Compat (lookupEnv, setEnv)+module Configuration.Dotenv+ ( load+ , loadFile+ , parseFile+ , onMissingFile )+ where import Configuration.Dotenv.Parse (configParser)-+import Control.Monad.Catch+import Control.Monad.IO.Class (MonadIO(..))+import System.Environment (lookupEnv)+import System.IO.Error (isDoesNotExistError) import Text.Megaparsec (parse) +#if MIN_VERSION_base(4,7,0)+import System.Environment (setEnv)+#else+import System.Environment.Compat (setEnv)+#endif+ -- | 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? -> [(String, String)] -- ^ List of values to be set in environment- -> IO ()+ -> m () load override = mapM_ (applySetting override) -- | Loads the options in the given file to the environment. Optionally -- override existing variables with values from Dotenv files. loadFile ::+ MonadIO m => Bool -- ^ Override existing settings? -> FilePath -- ^ A file containing options to load into the environment- -> IO ()+ -> m () loadFile override f = load override =<< parseFile f -- | Parses the given dotenv file and returns values /without/ adding them to -- the environment. parseFile ::+ MonadIO m => FilePath -- ^ A file containing options to read- -> IO [(String, String)] -- ^ Variables contained in the file+ -> m [(String, String)] -- ^ Variables contained in the file parseFile f = do- contents <- readFile f+ contents <- liftIO $ readFile f case parse configParser f contents of Left e -> error $ "Failed to read file" ++ show e Right options -> return options -applySetting :: Bool -> (String, String) -> IO ()+applySetting :: MonadIO m => Bool -> (String, String) -> m () applySetting override (key, value) = if override then- setEnv key value+ liftIO $ setEnv key value else do- res <- lookupEnv key+ res <- liftIO $ lookupEnv key case res of- Nothing -> setEnv key value+ Nothing -> liftIO $ setEnv key value Just _ -> return ()++-- | 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+ => m a -- ^ Action to perform that may fail because of missing file+ -> m a -- ^ Action to perform if file is indeed missing+ -> m a+onMissingFile f h = catchIf isDoesNotExistError f (const h)
src/Configuration/Dotenv/Parse.hs view
@@ -12,83 +12,80 @@ -- information on the dotenv format can be found in the project README and the -- test suite. -module Configuration.Dotenv.Parse (configParser) where+{-# LANGUAGE CPP #-} -import Text.Megaparsec ((<?>), anyChar, char, eof, manyTill, try)-import Text.Megaparsec.String (Parser)-import Text.Megaparsec.Char- (digitChar, letterChar, eol, noneOf, oneOf)+module Configuration.Dotenv.Parse (configParser) where import Control.Applicative-import Prelude--import Data.Maybe (catMaybes)-import Control.Monad (liftM2)+import Control.Monad+import Text.Megaparsec+import Text.Megaparsec.String (Parser)+import qualified Text.Megaparsec.Lexer as L --- | Returns a parser for a Dotenv configuration file. Accepts key--- and value arguments separated by "=". Comments are allowed on--- lines by themselves and on blank lines.+-- | Returns a parser for a Dotenv configuration file. Accepts key and value+-- arguments separated by @=@. Comments in all positions are handled+-- appropriately. configParser :: Parser [(String, String)]-configParser = catMaybes <$> many envLine <* eof---envLine :: Parser (Maybe (String, String))-envLine = (comment <|> blankLine) *> return Nothing <|> Just <$> optionLine--blankLine :: Parser String-blankLine = many verticalSpace <* eol <?> "blank line"+configParser = between scn eof (sepEndBy1 envLine (eol <* scn)) -optionLine :: Parser (String, String)-optionLine = liftM2 (,)- (many verticalSpace *> variableName <* variableValueSeparator)- value+-- | Parse a single environment variable assignment.+envLine :: Parser (String, String)+envLine = (,) <$> (lexeme variableName <* lexeme (char '=')) <*> lexeme value -- | Variables must start with a letter or underscore, and may contain -- letters, digits or '_' character after the first character. variableName :: Parser String-variableName = liftM2 (:) (letterChar <|> char '_')- (many (letterChar <|> char '_' <|> digitChar <?>- unwords [ "valid non-leading shell variable character (alphanumeric,"- , "digit or underscore)" ]))-- <?> unwords [ "shell variable name (letter or underscore followed"- , "by alphanumeric characters or underscores)" ]+variableName = ((:) <$> firstChar <*> many otherChar) <?> "variable name"+ where+ firstChar = char '_' <|> letterChar+ otherChar = firstChar <|> digitChar +-- | Value: quoted or unquoted. value :: Parser String-value = quotedValue <|> unquotedValue <?> "variable value"--quotedValue :: Parser String-quotedValue = (quotedWith '\'' <|> quotedWith '\"')- <* (comment *> return () <|> many verticalSpace *> endOfLineOrInput)- <?> "variable value surrounded with single or double quotes"--unquotedValue :: Parser String-unquotedValue =- manyTill anyChar (comment <|> many verticalSpace <* endOfLineOrInput)+value = (quotedValue <|> unquotedValue) <?> "variable value"+ where+ quotedValue = quotedWith '\'' <|> quotedWith '\"'+ unquotedValue = many (noneOf "\'\" \t\n\r") --- | Based on a commented-string parser in:--- http://hub.darcs.net/navilan/XMonadTasks/raw/Data/Config/Lexer.hs+-- | Parse a value quoted with given character. quotedWith :: Char -> Parser String-quotedWith c = char c *> many chr <* (char c <?> "closing quote character")+quotedWith q = between (char q) (char q) (many $ escapedChar <|> normalChar)+ where+ escapedChar = (char '\\' *> anyChar) <?> "escaped character"+ normalChar = noneOf (q : "\\") <?> "unescaped character" - where chr = esc <|> noneOf [c]- esc = escape *> char c <?> "escape character"+----------------------------------------------------------------------------+-- Boilerplate and whitespace setup -comment :: Parser String-comment = try (many verticalSpace *> char '#')- *> manyTill anyChar endOfLineOrInput- <?> "comment"+-- | Lexeme wrapper that takes care of consuming of white space.+lexeme :: Parser a -> Parser a+lexeme = L.lexeme sc+{-# INLINE lexeme #-} -endOfLineOrInput :: Parser ()-endOfLineOrInput = eol *> return () <|> eof+-- | Space consumer. Consumes all white space including comments, but never+-- consumes newlines.+sc :: Parser ()+sc = L.space (void spaceChar') skipLineComment empty+{-# INLINE sc #-} -variableValueSeparator :: Parser ()-variableValueSeparator =- many verticalSpace *> (char '=' <?> "variable-value separator character (=)")- *> many verticalSpace *> return ()+-- | Just like 'sc' but also eats newlines.+scn :: Parser ()+scn = L.space (void spaceChar) skipLineComment empty+{-# INLINE scn #-} -escape :: Parser Char-escape = char '\\'+-- | Just like 'spaceChar', but does not consume newlines.+spaceChar' :: Parser Char+spaceChar' = oneOf " \t"+{-# INLINE spaceChar' #-} -verticalSpace :: Parser Char-verticalSpace = oneOf " \t"+-- | Skip line comment and stop before newline character without consuming+-- it.+skipLineComment :: Parser ()+#if MIN_VERSION_megaparsec(5,1,0)+skipLineComment = L.skipLineComment "#"+#else+skipLineComment = p >> void (manyTill anyChar n)+ where p = string "#"+ n = lookAhead (void newline) <|> eof+#endif+{-# INLINE skipLineComment #-}
src/Configuration/Dotenv/Text.hs view
@@ -16,9 +16,14 @@ import qualified Data.Text as T import Control.Arrow ((***)) +import Control.Monad (liftM)++import Control.Monad.IO.Class (MonadIO(..))+ -- | Parses the given dotenv file and returns values /without/ adding them to -- the environment. parseFile ::+ MonadIO m => FilePath -- ^ A file containing options to read- -> IO [(T.Text, T.Text)] -- ^ Variables contained in the file-parseFile f = map (T.pack *** T.pack) `fmap` Configuration.Dotenv.parseFile f+ -> m [(T.Text, T.Text)] -- ^ Variables contained in the file+parseFile f = map (T.pack *** T.pack) `liftM` Configuration.Dotenv.parseFile f