├── stack.yaml ├── .gitignore ├── Setup.hs ├── test ├── Spec.hs ├── LoadEnvSpec.hs └── LoadEnv │ └── ParseSpec.hs ├── .hlint.yaml ├── doctest └── Main.hs ├── .github └── workflows │ ├── ci.yml │ └── nightly.yml ├── Makefile ├── .stylish-haskell.yaml ├── package.yaml ├── stack.yaml.lock ├── LICENSE ├── README.md ├── CHANGELOG.md └── src ├── LoadEnv └── Parse.hs └── LoadEnv.hs /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-18.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work 2 | *.cabal 3 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -F -pgmF hspec-discover #-} 2 | -------------------------------------------------------------------------------- /.hlint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - ignore: 3 | name: Redundant do 4 | within: spec 5 | -------------------------------------------------------------------------------- /doctest/Main.hs: -------------------------------------------------------------------------------- 1 | module Main (main) where 2 | 3 | import Test.DocTest 4 | 5 | main :: IO () 6 | main = doctest ["-isrc", "src/"] 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: main 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: freckle/stack-cache-action@main 14 | - uses: freckle/stack-action@main 15 | with: 16 | weeder: false 17 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: Stackage nightly 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: main 7 | 8 | schedule: 9 | - cron: "0 0 * * *" 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: freckle/stack-cache-action@main 17 | - uses: freckle/stack-action@main 18 | with: 19 | stack-arguments: --resolver nightly 20 | weeder: false 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: setup build test lint 2 | 3 | .PHONY: setup 4 | setup: 5 | stack setup 6 | stack build --dependencies-only --test --no-run-tests 7 | stack install hlint weeder 8 | 9 | .PHONY: build 10 | build: 11 | stack build --pedantic --test --no-run-tests 12 | 13 | .PHONY: test 14 | test: 15 | stack test 16 | 17 | 18 | .PHONY: lint 19 | lint: 20 | hlint . 21 | weeder . 22 | 23 | .PHONY: clean 24 | clean: 25 | stack clean 26 | 27 | .PHONY: check-nightly 28 | check-nightly: 29 | stack setup --resolver nightly 30 | stack build --resolver nightly --pedantic --test 31 | -------------------------------------------------------------------------------- /.stylish-haskell.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 3 | - simple_align: 4 | cases: false 5 | top_level_patterns: false 6 | records: false 7 | - imports: 8 | align: none 9 | list_align: after_alias 10 | pad_module_names: false 11 | long_list_align: new_line_multiline 12 | empty_list_align: right_after 13 | list_padding: 4 14 | separate_lists: false 15 | space_surround: false 16 | - language_pragmas: 17 | style: vertical 18 | align: false 19 | remove_redundant: true 20 | - trailing_whitespace: {} 21 | columns: 80 22 | newline: native 23 | -------------------------------------------------------------------------------- /package.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: load-env 3 | version: '0.2.1.0' 4 | synopsis: Load environment variables from a file. 5 | description: > 6 | Parse a .env file and load any declared variables into the current process's 7 | environment. This allows for a .env file to specify development-friendly 8 | defaults for configuration values normally set in the deployment environment. 9 | category: Configuration 10 | author: Pat Brisbin 11 | maintainer: Pat Brisbin 12 | license: BSD3 13 | github: pbrisbin/load-env 14 | extra-source-files: 15 | - README.md 16 | - CHANGELOG.md 17 | 18 | dependencies: 19 | - base >=4.8.0 && <5 20 | 21 | ghc-options: -Wall 22 | 23 | library: 24 | source-dirs: src 25 | dependencies: 26 | - directory 27 | - filepath 28 | - filepath 29 | - parsec 30 | 31 | tests: 32 | spec: 33 | main: Spec.hs 34 | source-dirs: test 35 | dependencies: 36 | - directory 37 | - hspec 38 | - load-env 39 | - parsec 40 | - temporary 41 | doctest: 42 | main: Main.hs 43 | source-dirs: doctest 44 | dependencies: 45 | - doctest 46 | -------------------------------------------------------------------------------- /stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: 7 | - completed: 8 | hackage: dhall-1.37.1@sha256:447031286e8fe270b0baacd9cc5a8af340d2ae94bb53b85807bee93381ca5287,35080 9 | pantry-tree: 10 | size: 305799 11 | sha256: 623de5587e614ace2b6e2f908ccd4b4eec26db9cf29de45874bed8c0bdef2db8 12 | original: 13 | hackage: dhall-1.37.1@sha256:447031286e8fe270b0baacd9cc5a8af340d2ae94bb53b85807bee93381ca5287,35080 14 | - completed: 15 | hackage: generic-lens-2.0.0.0@sha256:7409fa0ce540d0bd41acf596edd1c5d0c0ab1cd1294d514cf19c5c24e8ef2550,3866 16 | pantry-tree: 17 | size: 2470 18 | sha256: 46ba160f0efc9c805eac6666f298f48dda899834b68c860f63641ce1f82db737 19 | original: 20 | hackage: generic-lens-2.0.0.0@sha256:7409fa0ce540d0bd41acf596edd1c5d0c0ab1cd1294d514cf19c5c24e8ef2550,3866 21 | snapshots: 22 | - completed: 23 | size: 585393 24 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/0.yaml 25 | sha256: c632012da648385b9fa3c29f4e0afd56ead299f1c5528ee789058be410e883c0 26 | original: lts-18.0 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Pat Brisbin 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Pat Brisbin nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # load-env 2 | 3 | **Archived**: this project is no longer seeing updates. I (highly) recommend 4 | you migrate to [dotenv-hs][], which has the same usage, the same features, and 5 | much more. 6 | 7 | [dotenv-hs]: https://github.com/stackbuilders/dotenv-hs#readme 8 | 9 | [![Hackage](https://img.shields.io/hackage/v/load-env.svg?style=flat)](https://hackage.haskell.org/package/load-env) 10 | [![Stackage Nightly](http://stackage.org/package/load-env/badge/nightly)](http://stackage.org/nightly/package/load-env) 11 | [![Stackage LTS](http://stackage.org/package/load-env/badge/lts)](http://stackage.org/lts/package/shellwords) 12 | [![CI](https://github.com/pbrisbin/load-env/actions/workflows/ci.yml/badge.svg)](https://github.com/pbrisbin/load-env/actions/workflows/ci.yml) 13 | 14 | This is effectively a port of [dotenv][], whose README explains it best: 15 | 16 | > Storing configuration in the environment is one of the tenets of a 17 | > twelve-factor app. Anything that is likely to change between deployment 18 | > environments–such as resource handles for databases or credentials for 19 | > external services–should be extracted from the code into environment 20 | > variables. 21 | > 22 | > But it is not always practical to set environment variables on development 23 | > machines or continuous integration servers where multiple projects are run. 24 | > dotenv loads variables from a .env file into ENV when the environment is 25 | > bootstrapped. 26 | 27 | [dotenv]: https://github.com/bkeepers/dotenv 28 | 29 | This library exposes functions for doing just that. 30 | 31 | ## Usage 32 | 33 | ```haskell 34 | import LoadEnv 35 | import System.Environment (lookupEnv) 36 | 37 | main :: IO () 38 | main = do 39 | loadEnv 40 | 41 | print =<< lookupEnv "FOO" 42 | ``` 43 | 44 | ```console 45 | % cat .env 46 | FOO=bar 47 | % runhaskell main.hs 48 | Just "bar" 49 | ``` 50 | 51 | ## Development & Test 52 | 53 | ``` 54 | stack setup 55 | stack build --pedantic --test 56 | ``` 57 | 58 | --- 59 | 60 | [CHANGELOG](./CHANGELOG.md) | [LICENSE](./LICENSE) 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [_Unreleased_](https://github.com/pbrisbin/load-env/compare/v0.2.1.0...main) 2 | 3 | None 4 | 5 | ## [v0.2.1.0](https://github.com/pbrisbin/load-env/compare/v0.2.0.2...v0.2.1.0) 6 | 7 | - Don't override values already set in the environment 8 | 9 | Given a hypothetical program `load-env`, which uses one of our `loadEnv` 10 | functions on `stdin`: 11 | 12 | Previously, 13 | 14 | ``` 15 | FOO=bar load-env < many parseLine 18 | 19 | parseLine :: Parser (Maybe Variable) 20 | parseLine = possibly parseVariable 21 | 22 | possibly :: Parser a -> Parser (Maybe a) 23 | possibly p = try (Just <$> p) <|> ignored 24 | 25 | where 26 | ignored = Nothing <$ manyTill anyToken newline 27 | 28 | parseVariable :: Parser Variable 29 | parseVariable = do 30 | optional $ between spaces spaces $ string "export" 31 | 32 | i <- identifier 33 | v <- char '=' *> value 34 | 35 | void $ many $ oneOf " \t" 36 | void newline 37 | 38 | pure (i, v) 39 | 40 | -- Environment variable names used by the utilities in the Shell and Utilities 41 | -- volume of POSIX.1-2017 consist solely of uppercase letters, digits, 42 | -- and the ( '_' ) from the characters defined in Portable 43 | -- Character Set and do not begin with a digit. Other characters may be 44 | -- permitted by an implementation; applications shall tolerate the presence 45 | -- of such names. Uppercase and lowercase letters shall retain their unique 46 | -- identities and shall not be folded together. The name space of environment 47 | -- variable names containing lowercase letters is reserved for applications. 48 | -- Applications can define any environment variables with names from this name 49 | -- space without modifying the behavior of the standard utilities. 50 | -- 51 | -- 52 | -- 53 | identifier :: Parser String 54 | identifier = do 55 | x <- upper <|> lower <|> underscore 56 | ys <- many $ upper <|> lower <|> digit <|> underscore 57 | 58 | pure (x:ys) 59 | 60 | where 61 | underscore = char '_' 62 | 63 | value :: Parser String 64 | value = quotedValue <|> unquotedValue <|> pure "" 65 | 66 | quotedValue :: Parser String 67 | quotedValue = do 68 | q <- oneOf "'\"" 69 | 70 | manyTill (try (escaped q) <|> anyToken) (char q) 71 | 72 | unquotedValue :: Parser String 73 | unquotedValue = many1 $ try (escaped ' ') <|> noneOf "\"' \n" 74 | 75 | escaped :: Char -> Parser Char 76 | escaped c = c <$ string ("\\" ++ [c]) 77 | -------------------------------------------------------------------------------- /test/LoadEnvSpec.hs: -------------------------------------------------------------------------------- 1 | module LoadEnvSpec 2 | ( spec 3 | ) where 4 | 5 | import Control.Monad (when) 6 | import LoadEnv 7 | import System.Directory 8 | import System.Environment 9 | import System.IO.Temp 10 | import Test.Hspec 11 | 12 | spec :: Spec 13 | spec = after_ cleanup $ do 14 | describe "loadEnv" $ do 15 | it "loads environment variables from ./.env if present" $ do 16 | writeFile envFile $ unlines 17 | [ "FOO=\"bar\"" 18 | , "BAZ=\"bat\"" 19 | ] 20 | 21 | loadEnvFrom envFile 22 | 23 | mbar <- lookupEnv "FOO" 24 | mbat <- lookupEnv "BAZ" 25 | mbar `shouldBe` Just "bar" 26 | mbat `shouldBe` Just "bat" 27 | 28 | it "does not override pre-existing variables" $ do 29 | writeFile envFile $ unlines ["FOO=bar"] 30 | setEnv "FOO" "baz" 31 | 32 | loadEnvFrom envFile 33 | 34 | mbar <- lookupEnv "FOO" 35 | mbar `shouldBe` Just "baz" 36 | 37 | it "does not fail if the file is not present" $ do 38 | loadEnvFrom "i-do-not-exist" 39 | 40 | return () 41 | 42 | describe "loadEnvFrom" $ do 43 | it "traverses up the directory tree" $ do 44 | inTempDirectory $ do 45 | writeFile ".env.test" "FOO=\"bar\"\n" 46 | inNewDirectory "foo/bar/baz" $ do 47 | loadEnvFrom ".env.test" 48 | 49 | lookupEnv "FOO" `shouldReturn` Just "bar" 50 | 51 | it "loads only the nearest file" $ do 52 | inTempDirectory $ do 53 | writeFile ".env.test" "FOO=\"bar\"\n" 54 | inNewDirectory "foo/bar" $ do 55 | writeFile ".env.test" "BAR=\"baz\"\n" 56 | inNewDirectory "baz/bat" $ do 57 | loadEnvFrom ".env.test" 58 | 59 | lookupEnv "BAR" `shouldReturn` Just "baz" 60 | lookupEnv "FOO" `shouldReturn` Nothing 61 | 62 | describe "loadEnvFromAbsolute" $ do 63 | it "does not traverse up the directory tree" $ do 64 | inTempDirectory $ do 65 | writeFile ".env.test" "FOO=\"bar\"\n" 66 | inNewDirectory "foo/bar/baz" $ do 67 | loadEnvFromAbsolute ".env.test" 68 | 69 | lookupEnv "FOO" `shouldReturn` Nothing 70 | 71 | inTempDirectory :: IO a -> IO a 72 | inTempDirectory f = 73 | withSystemTempDirectory "" $ \tmp -> withCurrentDirectory tmp f 74 | 75 | inNewDirectory :: FilePath -> IO a -> IO a 76 | inNewDirectory path f = do 77 | createDirectoryIfMissing True path 78 | withCurrentDirectory path f 79 | 80 | cleanup :: IO () 81 | cleanup = do 82 | unsetEnv "FOO" 83 | unsetEnv "BAR" 84 | e <- doesFileExist envFile 85 | when e $ removeFile envFile 86 | 87 | envFile :: FilePath 88 | envFile = "/tmp/load-env-test-file" 89 | -------------------------------------------------------------------------------- /src/LoadEnv.hs: -------------------------------------------------------------------------------- 1 | -- | 2 | -- 3 | -- This is effectively a port of dotenv, whose README explains it best: 4 | -- 5 | -- > Storing configuration in the environment is one of the tenets of a 6 | -- > twelve-factor app. Anything that is likely to change between deployment 7 | -- > environments–such as resource handles for databases or credentials for 8 | -- > external services–should be extracted from the code into environment 9 | -- > variables. 10 | -- > 11 | -- > But it is not always practical to set environment variables on development 12 | -- > machines or continuous integration servers where multiple projects are run. 13 | -- > dotenv loads variables from a .env file into ENV when the environment is 14 | -- > bootstrapped. 15 | -- 16 | -- 17 | -- 18 | -- This library exposes functions for doing just that. 19 | -- 20 | module LoadEnv 21 | ( loadEnv 22 | , loadEnvFrom 23 | , loadEnvFromAbsolute 24 | ) where 25 | 26 | import Control.Monad (unless, (<=<)) 27 | import Data.Bool (bool) 28 | import Data.Foldable (for_, traverse_) 29 | import Data.List (inits) 30 | import Data.Maybe (isJust) 31 | import LoadEnv.Parse 32 | import System.Directory 33 | (doesFileExist, findFile, getCurrentDirectory, makeAbsolute) 34 | import System.Environment (lookupEnv, setEnv) 35 | import System.FilePath (isRelative, joinPath, splitDirectories) 36 | import Text.Parsec.String (parseFromFile) 37 | 38 | -- | @'loadEnvFrom' \".env\"@ 39 | loadEnv :: IO () 40 | loadEnv = loadEnvFrom ".env" 41 | 42 | -- | Parse the given file and set variables in the process's environment 43 | -- 44 | -- Variables can be declared in the following form: 45 | -- 46 | -- > FOO=bar 47 | -- > FOO="bar" 48 | -- > FOO='bar' 49 | -- 50 | -- Declarations may optionally be preceded by @\"export \"@, which will be 51 | -- ignored. Trailing whitespace is ignored. Quotes inside quoted values or 52 | -- spaces in unquoted values must be escaped with a backlash. Invalid lines are 53 | -- silently ignored. 54 | -- 55 | -- __NOTE__: If the file-name is relative, the directory tree will be traversed 56 | -- up to @\/@ looking for the file in each parent. Use @'loadEnvFromAbsolute'@ 57 | -- to avoid this. 58 | -- 59 | loadEnvFrom :: FilePath -> IO () 60 | loadEnvFrom name = do 61 | mFile <- if isRelative name 62 | then flip findFile name . takeDirectories =<< getCurrentDirectory 63 | else bool Nothing (Just name) <$> doesFileExist name 64 | 65 | for_ mFile $ \file -> do 66 | result <- parseFromFile parseEnvironment file 67 | either print (traverse_ $ uncurry defaultEnv) result 68 | 69 | defaultEnv :: String -> String -> IO () 70 | defaultEnv k v = do 71 | exists <- isJust <$> lookupEnv k 72 | unless exists $ setEnv k v 73 | 74 | -- | @'loadEnvFrom'@, but don't traverse up the directory tree 75 | loadEnvFromAbsolute :: FilePath -> IO () 76 | loadEnvFromAbsolute = loadEnvFrom <=< makeAbsolute 77 | 78 | -- | Get all directory names of a directory 79 | -- 80 | -- Includes itself as the first element of the output. 81 | -- 82 | -- >>> takeDirectories "/foo/bar/baz" 83 | -- ["/foo/bar/baz","/foo/bar","/foo","/"] 84 | -- 85 | -- Leading path-separator is meaningful, and determines if the root directory is 86 | -- included or not. 87 | -- 88 | -- >>> takeDirectories "foo/bar/baz" 89 | -- ["foo/bar/baz","foo/bar","foo"] 90 | -- 91 | -- Trailing path-separator is not meaningful. 92 | -- 93 | -- >>> takeDirectories "/foo/bar/baz/" 94 | -- ["/foo/bar/baz","/foo/bar","/foo","/"] 95 | -- 96 | takeDirectories :: FilePath -> [FilePath] 97 | takeDirectories = map joinPath . reverse . drop 1 . inits . splitDirectories 98 | -------------------------------------------------------------------------------- /test/LoadEnv/ParseSpec.hs: -------------------------------------------------------------------------------- 1 | module LoadEnv.ParseSpec 2 | ( spec 3 | ) where 4 | 5 | import LoadEnv.Parse 6 | import Test.Hspec 7 | import Text.Parsec (parse) 8 | 9 | spec :: Spec 10 | spec = do 11 | describe "parseEnvironment" $ do 12 | it "parses variable declarations among comments and blank lines" $ do 13 | let env = unlines 14 | [ "# An environment file" 15 | , "FOO=bar" 16 | , "BAZ=\"bat\"" 17 | , "BAT=\"multi-" 18 | , "pass" 19 | , "\"" 20 | , "" 21 | , "# vim ft:sh:" 22 | ] 23 | 24 | parse parseEnvironment "" env `shouldBe` Right 25 | [ ("FOO", "bar") 26 | , ("BAZ", "bat") 27 | , ("BAT", "multi-\npass\n") 28 | ] 29 | 30 | it "parses an empty file into an empty list of variables" $ do 31 | parse parseEnvironment "" "" `shouldBe` Right [] 32 | 33 | 34 | describe "parseVariable" $ do 35 | it "reads unquoted variables" $ 36 | parse parseVariable "" "FOO=bar\n" `shouldBe` Right ("FOO", "bar") 37 | 38 | it "reads quoted variables" $ do 39 | parse parseVariable "" "FOO=\"bar\"\n" 40 | `shouldBe` Right ("FOO", "bar") 41 | parse parseVariable "" "FOO='bar'\n" 42 | `shouldBe` Right ("FOO", "bar") 43 | 44 | it "allows newlines in quoted variables" $ do 45 | parse parseVariable "" "FOO=\"foo\nbar\"\n" 46 | `shouldBe` Right ("FOO", "foo\nbar") 47 | 48 | it "handles empty values" $ 49 | parse parseVariable "" "FOO=\n" `shouldBe` Right ("FOO", "") 50 | 51 | it "handles empty quoted values" $ do 52 | parse parseVariable "" "FOO=\"\"\n" `shouldBe` Right ("FOO", "") 53 | parse parseVariable "" "FOO=''\n" `shouldBe` Right ("FOO", "") 54 | 55 | it "handles underscored variables" $ 56 | parse parseVariable "" "FOO_BAR=baz\n" 57 | `shouldBe` Right ("FOO_BAR", "baz") 58 | 59 | it "treats leading spaces as invalid" $ 60 | parse parseVariable "" " FOO=bar\n" 61 | `shouldContainError` "unexpected \"F\"" 62 | 63 | it "treats spaces around equals as invalid" $ 64 | parse parseVariable "" "FOO = bar\n" 65 | `shouldContainError` "unexpected \" \"" 66 | 67 | it "treats unquoted spaces as invalid" $ 68 | parse parseVariable "" "FOO=bar baz\n" 69 | `shouldContainError` "unexpected \"b\"" 70 | 71 | it "treats unbalanced quotes as invalid" $ do 72 | parse parseVariable "" "FOO=\"bar\n" 73 | `shouldContainError` "unexpected end of input" 74 | parse parseVariable "" "FOO='bar\n" 75 | `shouldContainError` "unexpected end of input" 76 | parse parseVariable "" "FOO=bar\"\n" 77 | `shouldContainError` "unexpected \"\\\"\"" 78 | parse parseVariable "" "FOO=bar'\n" 79 | `shouldContainError` "unexpected \"\'\"" 80 | 81 | it "handles escaped quotes" $ do 82 | parse parseVariable "" "FOO=\"bar\\\"baz\"\n" 83 | `shouldBe` Right ("FOO", "bar\"baz") 84 | parse parseVariable "" "FOO='bar\\'baz'\n" 85 | `shouldBe` Right ("FOO", "bar'baz") 86 | 87 | it "handles escaped spaces" $ 88 | parse parseVariable "" "FOO=bar\\ baz\n" 89 | `shouldBe` Right ("FOO", "bar baz") 90 | 91 | it "discards any lines using `export'" $ 92 | parse parseVariable "" "export FOO=bar\n" 93 | `shouldBe` Right ("FOO", "bar") 94 | 95 | it "requires valid environment variable identifies" $ do 96 | parse parseVariable "" "S3_KEY=abc123\n" 97 | `shouldBe` Right ("S3_KEY", "abc123") 98 | parse parseVariable "" "_S3_KEY=abc123\n" 99 | `shouldBe` Right ("_S3_KEY", "abc123") 100 | parse parseVariable "" "S3_key=abc123\n" 101 | `shouldBe` Right ("S3_key", "abc123") 102 | parse parseVariable "" "s3_key=abc123\n" 103 | `shouldBe` Right ("s3_key", "abc123") 104 | 105 | parse parseVariable "" "S3~KEY=abc123\n" 106 | `shouldContainError` "unexpected \"~\"" 107 | parse parseVariable "" "S3-KEY=abc123\n" 108 | `shouldContainError` "unexpected \"-\"" 109 | parse parseVariable "" "3_KEY=abc123\n" 110 | `shouldContainError` "unexpected \"3\"" 111 | 112 | shouldContainError :: Show a => Either a b -> String -> Expectation 113 | v `shouldContainError` msg = either 114 | (\e -> show e `shouldContain` msg) 115 | (\_ -> expectationFailure "Expected no parse") v 116 | --------------------------------------------------------------------------------