Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# This supports the example; see README.md.

LIBRARY_NAME=dotenv
TEST_VAR="Greetings from ${LIBRARY_NAME}"
TEST_VAR="Greetings from ${LIBRARY_NAME}, $(whoami)!"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
/.purs*
/.psa*
/.spago
*.swp
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

*0.4.0*
* Added support for command substitution.

*0.3.0*
* Updated library to support new PureScript compiler version 0.13.
* Migrated to Spago for dev/CI.
Expand Down
6 changes: 4 additions & 2 deletions bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@
"dependencies": {
"purescript-console": "^4.2.0",
"purescript-effect": "^2.0.1",
"purescript-node-fs-aff": "^6.0.0",
"purescript-node-process": "^7.0.0",
"purescript-parsing": "^5.0.3",
"purescript-node-fs-aff": "^6.0.0"
"purescript-run": "^3.0.1",
"purescript-sunde": "^2.0.0"
},
"devDependencies": {
"purescript-psci-support": "^4.0.0",
"purescript-spec": "^4.0.0-rc.1"
"purescript-spec": "^4.0.0"
}
}
4 changes: 2 additions & 2 deletions packages.dhall
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
let mkPackage =
https://github.com/purescript/package-sets/psc-0.13.0-20190607/src/mkPackage.dhall sha256:0b197efa1d397ace6eb46b243ff2d73a3da5638d8d0ac8473e8e4a8fc528cf57
https://github.com/purescript/package-sets/psc-0.13.0-20190626/src/mkPackage.dhall sha256:0b197efa1d397ace6eb46b243ff2d73a3da5638d8d0ac8473e8e4a8fc528cf57

let upstream =
https://github.com/purescript/package-sets/psc-0.13.0-20190607/src/packages.dhall sha256:96b28e434b8a62caea5f10376b4f7dc1736a668592cabe914f117ecf5673c2ff
https://github.com/purescript/package-sets/psc-0.13.0-20190626/src/packages.dhall sha256:9905f07c9c3bd62fb3205e2108515811a89d55cff24f4341652f61ddacfcf148

let overrides = {=}

Expand Down
11 changes: 10 additions & 1 deletion spago.dhall
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
{ name =
"dotenv"
, dependencies =
[ "console", "effect", "node-fs-aff", "node-process", "parsing", "psci-support", "spec" ]
[ "console"
, "effect"
, "node-fs-aff"
, "node-process"
, "parsing"
, "psci-support"
, "run"
, "spec"
, "sunde"
]
, packages =
./packages.dhall
}
75 changes: 38 additions & 37 deletions src/Dotenv.purs
Original file line number Diff line number Diff line change
@@ -1,54 +1,55 @@
-- | This is the base module for the Dotenv library.

module Dotenv (module Dotenv.Types, loadFile) where
module Dotenv (Name, Setting, Settings, Value, loadFile) where

import Prelude
import Control.Monad.Error.Class (class MonadThrow, catchError, throwError)
import Data.Either (either)
import Data.Maybe (Maybe(..))
import Data.Traversable (traverse)
import Data.Tuple (Tuple(..))
import Data.Maybe (Maybe)
import Data.Tuple (Tuple)
import Dotenv.Internal.Apply (applySettings)
import Dotenv.Internal.ChildProcess (_childProcess, handleChildProcess)
import Dotenv.Internal.Environment (_environment, handleEnvironment)
import Dotenv.Internal.Parse (settings) as Parse
import Dotenv.Internal.Resolve (values) as Resolve
import Dotenv.Internal.Types (Settings) as Internal
import Dotenv.Types (Setting, Settings)
import Dotenv.Internal.Resolve (resolveValues)
import Dotenv.Internal.Types (Setting) as IT
import Dotenv.Internal.Types (UnresolvedValue)
import Effect.Aff.Class (class MonadAff, liftAff)
import Effect.Class (liftEffect)
import Effect.Exception (Error, error)
import Node.Encoding (Encoding(UTF8))
import Node.FS.Aff (readTextFile)
import Node.Process (getEnv, lookupEnv, setEnv)
import Run (case_, interpret, on)
import Text.Parsing.Parser (parseErrorMessage, runParser)

-- The type of a setting name
type Name = String

-- The type of a (resolved) value
type Value = Maybe String

-- The type of a setting
type Setting = Tuple Name Value

-- The type of settings
type Settings = Array Setting

-- | Loads the `.env` file into the environment.
loadFile :: forall m. MonadAff m => MonadThrow Error m => m Settings
loadFile = (Resolve.values <$> liftEffect getEnv <*> (readSettings >>= parseSettings)) >>= applySettings
loadFile = readDotenv
>>= (flip runParser Parse.settings >>> either (parseErrorMessage >>> error >>> throwError) pure)
>>= processSettings

-- | Reads the `.env` file.
readSettings :: forall m. MonadAff m => m String
readSettings = liftAff $ readTextFile UTF8 ".env"
# flip catchError (const $ pure "")

-- | Parses the contents of a `.env` file.
parseSettings :: forall m. MonadThrow Error m => String -> m Internal.Settings
parseSettings settings = runParser settings Parse.settings
# either (throwError <<< error <<< append "Invalid .env file: " <<< parseErrorMessage) pure

-- | Applies the specified settings to the environment.
applySettings :: forall m. MonadAff m => Settings -> m Settings
applySettings = traverse applySetting

-- | Applies the specified setting to the environment.
applySetting :: forall m. MonadAff m => Setting -> m Setting
applySetting setting@(Tuple key settingValue) = do
envValue <- liftEffect $ lookupEnv key
case envValue of
Just value ->
pure $ Tuple key $ Just value
Nothing ->
case settingValue of
Just value -> do
liftEffect $ setEnv key value
pure setting
Nothing ->
pure setting
readDotenv :: forall m. MonadAff m => m String
readDotenv = liftAff $ readTextFile UTF8 ".env"
# flip catchError (const $ pure "")

-- | Processes settings by resolving their values and then applying them to the environment.
processSettings :: forall m. MonadAff m => Array (IT.Setting UnresolvedValue) -> m Settings
processSettings = (resolveValues >=> applySettings)
>>> interpret
( case_
# on _childProcess handleChildProcess
# on _environment handleEnvironment
)
>>> liftAff
24 changes: 24 additions & 0 deletions src/Dotenv/Internal/Apply.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-- | This module encapsulates the logic for applying settings to the environment.

module Dotenv.Internal.Apply (applySettings) where

import Prelude
import Data.Maybe (fromMaybe, isJust)
import Data.Traversable (traverse)
import Data.Tuple (Tuple(..))
import Dotenv.Internal.Environment (ENVIRONMENT, lookupEnv, setEnv)
import Dotenv.Internal.Types (ResolvedValue, Setting)
import Run (Run)

-- | Applies the specified settings to the environment.
applySettings
:: forall r
. Array (Setting ResolvedValue)
-> Run (environment :: ENVIRONMENT | r) (Array (Setting ResolvedValue))
applySettings = traverse \(Tuple name resolvedValue) -> do
currentValue <- lookupEnv name
if isJust currentValue
then pure $ Tuple name currentValue
else do
when (isJust resolvedValue) (setEnv name $ fromMaybe "" resolvedValue)
pure $ Tuple name resolvedValue
40 changes: 40 additions & 0 deletions src/Dotenv/Internal/ChildProcess.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
-- | This module encapsulates the logic for running a child process.

module Dotenv.Internal.ChildProcess (CHILD_PROCESS, ChildProcessF(..), _childProcess, handleChildProcess, spawn) where

import Prelude
import Control.Monad.Error.Class (throwError)
import Data.Maybe (Maybe(Nothing))
import Data.Symbol (SProxy(..))
import Effect.Aff (Aff)
import Effect.Exception (error)
import Node.ChildProcess (Exit(..), defaultSpawnOptions)
import Run (FProxy, Run, lift)
import Sunde (spawn) as Sunde

-- | A data type representing the supported operations
data ChildProcessF a = Spawn String (Array String) (String -> a)

derive instance functorChildProcessF :: Functor ChildProcessF

-- | The effect label used for a child process
_childProcess = SProxy :: SProxy "childProcess"

-- | The effect type used for a child process
type CHILD_PROCESS = FProxy (ChildProcessF)

-- | The default interpreter for handling a child process
handleChildProcess :: ChildProcessF ~> Aff
handleChildProcess (Spawn cmd args callback) = do
{ stderr, stdout, exit } <- Sunde.spawn { cmd, args, stdin: Nothing } defaultSpawnOptions
case exit of
Normally 0 ->
pure $ callback stdout
Normally code ->
throwError (error $ "Exited with code " <> show code <> ": " <> stderr)
BySignal signal ->
throwError (error $ "Exited: " <> show signal)

-- | Constructs the value used to spawn a child process.
spawn :: forall r. String -> Array String -> Run (childProcess :: CHILD_PROCESS | r) String
spawn cmd args = lift _childProcess (Spawn cmd args identity)
49 changes: 49 additions & 0 deletions src/Dotenv/Internal/Environment.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
-- | This module encapsulates the logic for reading or modifying the environment.
module Dotenv.Internal.Environment
( ENVIRONMENT
, EnvironmentF(..)
, _environment
, handleEnvironment
, lookupEnv
, setEnv
) where

import Prelude
import Data.Maybe (Maybe)
import Data.Symbol (SProxy(..))
import Effect.Aff (Aff)
import Effect.Class (liftEffect)
import Node.Process (lookupEnv, setEnv) as P
import Run (FProxy, Run, lift)

-- | A data type representing the supported operations.
data EnvironmentF a
= LookupEnv String (Maybe String -> a)
| SetEnv String String a

derive instance functorEnvironmentF :: Functor EnvironmentF

-- The effect label used for reading or modifying the environment.
_environment = SProxy :: SProxy "environment"

-- | The effect type used for reading or modifying the environment
type ENVIRONMENT = FProxy (EnvironmentF)

-- | The default interpreter used for reading or modifying the environment
handleEnvironment :: EnvironmentF ~> Aff
handleEnvironment op = liftEffect $
case op of
LookupEnv name callback -> do
value <- P.lookupEnv name
pure $ callback value
SetEnv name value next -> do
P.setEnv name value
pure next

-- | Constructs the value used to look up an environment variable.
lookupEnv :: forall r. String -> Run (environment :: ENVIRONMENT | r) (Maybe String)
lookupEnv name = lift _environment (LookupEnv name identity)

-- | Constructs the value used to set an environment variable.
setEnv :: forall r. String -> String -> Run (environment :: ENVIRONMENT | r) Unit
setEnv name value = lift _environment (SetEnv name value unit)
48 changes: 31 additions & 17 deletions src/Dotenv/Internal/Parse.purs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
-- | This module encapsulates the parsing logic for a `.env` file.

module Dotenv.Internal.Parse (settings) where
module Dotenv.Internal.Parse where

import Prelude hiding (between)
import Control.Alt ((<|>))
import Data.Array (fromFoldable, head, length, many, some)
import Data.Array ((:), fromFoldable, head, length, many, some)
import Data.Maybe (fromMaybe)
import Data.String.CodeUnits (fromCharArray)
import Data.Tuple (Tuple(..))
import Dotenv.Internal.Types (Name, Setting, Settings, Value(..))
import Dotenv.Internal.Types (Name, Setting, UnresolvedValue(..))
import Text.Parsing.Parser (Parser)
import Text.Parsing.Parser.Combinators ((<?>), lookAhead, notFollowedBy, skipMany, sepEndBy, try)
import Text.Parsing.Parser.String (char, noneOf, oneOf, string, whiteSpace)
Expand All @@ -23,7 +23,7 @@ whitespaceChars :: Array Char
whitespaceChars = [' ', '\t']

-- | Parses `.env` settings.
settings :: Parser String Settings
settings :: Parser String (Array (Setting UnresolvedValue))
settings = fromFoldable <$> do
skipMany notSetting
(setting <* many (noneOf newlineChars)) `sepEndBy` skipMany notSetting
Expand All @@ -39,37 +39,51 @@ name :: Parser String Name
name = fromCharArray <$> many (alphaNum <|> char '_') <* char '='

-- | Parses a variable substitution, i.e. `${VARIABLE_NAME}`.
variableSubstitution :: Parser String Value
variableSubstitution :: Parser String UnresolvedValue
variableSubstitution =
string "${" *> (VariableSubstitution <<< fromCharArray <$> some (alphaNum <|> char '_')) <* char '}'

-- | Parses a command substitution, i.e. `$(whoami)`.
commandSubstitution :: Parser String UnresolvedValue
commandSubstitution = do
_ <- string "$("
command <- fromCharArray <$> (some $ noneOf (')' : whitespaceChars))
arguments <- many $ whiteSpace *> (fromCharArray <$> (some $ noneOf (')' : whitespaceChars)))
_ <- whiteSpace *> char ')'
pure $ CommandSubstitution command arguments

-- | Parses a quoted value, enclosed in the specified type of quotation mark.
quotedValue :: Char -> Parser String Value
quotedValue q = valueFromValues <$> (char q *> (some $ variableSubstitution <|> literal) <* char q)
where
literal = LiteralValue <<< fromCharArray <$> some (noneOf ['$', q] <|> try (char '$' <* notFollowedBy (char '{')))
quotedValue :: Char -> Parser String UnresolvedValue
quotedValue q =
let
literal =
LiteralValue <<< fromCharArray <$> some (noneOf ['$', q] <|> try (char '$' <* notFollowedBy (oneOf ['{', '('])))
in
valueFromValues <$> (char q *> (some $ variableSubstitution <|> commandSubstitution <|> literal) <* char q)

-- | Parses an unquoted value.
unquotedValue :: Parser String Value
unquotedValue = valueFromValues <$> (whiteSpace *> (some $ variableSubstitution <|> literal))
where
unquotedValue :: Parser String UnresolvedValue
unquotedValue =
let
literal = map
( LiteralValue <<< fromCharArray)
( LiteralValue <<< fromCharArray )
$ some
$ try (noneOf (['$', '#'] <> whitespaceChars <> newlineChars))
<|> try (char '$' <* notFollowedBy (char '{'))
<|> try (char '$' <* notFollowedBy (oneOf ['{', '(']))
<|> try (oneOf whitespaceChars <* lookAhead (noneOf $ ['#'] <> whitespaceChars <> newlineChars))
in
valueFromValues <$> (whiteSpace *> (some $ variableSubstitution <|> commandSubstitution <|> literal))

-- | Assembles a single value from a series of values.
valueFromValues :: Array Value -> Value
valueFromValues :: Array UnresolvedValue -> UnresolvedValue
valueFromValues v
| length v == 1 = fromMaybe (ValueExpression []) (head v)
| otherwise = ValueExpression v

-- | Parses a setting value.
value :: Parser String Value
value :: Parser String UnresolvedValue
value = (quotedValue '"' <|> quotedValue '\'' <|> unquotedValue) <?> "variable value"

-- | Parses a setting in the form of `NAME=value`.
setting :: Parser String Setting
setting :: Parser String (Setting UnresolvedValue)
setting = Tuple <$> name <*> value
Loading