packages feed

agentic-io-0.2.0.0: src/Agentic/IO/DotEnv.hs

-- | Loading provider keys from a @.env@ file.
module Agentic.IO.DotEnv
  ( loadDotEnv
  , parseDotEnv
  ) where

import Control.Monad (filterM, forM_, when)
import Data.Char (isSpace)
import Data.List (dropWhileEnd, isPrefixOf)
import Data.Maybe (isNothing, listToMaybe)
import System.Directory (doesFileExist, getCurrentDirectory)
import System.Environment (lookupEnv, setEnv)
import System.FilePath (takeDirectory, (</>))

-- | Find the nearest @.env@, from the current directory upwards, and set each
-- variable it defines that isn't already set. Returns the file it loaded.
loadDotEnv :: IO (Maybe FilePath)
loadDotEnv = do
  here <- getCurrentDirectory
  found <- listToMaybe <$> filterM doesFileExist (map (</> ".env") (upwards here))
  forM_ found $ \file -> do
    vars <- parseDotEnv <$> readFile file
    forM_ vars $ \(key, value) -> do
      existing <- lookupEnv key
      when (isNothing existing && not (null value)) (setEnv key value)
  pure found
  where
    upwards dir
      | takeDirectory dir == dir = [dir]
      | otherwise = dir : upwards (takeDirectory dir)

-- | @KEY=value@ lines. Blank lines and @#@ comments are skipped, an @export@
-- prefix is allowed, and matching quotes around a value are removed.
parseDotEnv :: String -> [(String, String)]
parseDotEnv = concatMap entry . lines
  where
    entry raw = case break (== '=') (strip (dropExport (strip raw))) of
      (key, '=' : value) | not (null key), not ("#" `isPrefixOf` key) -> [(strip key, unquote (strip value))]
      _ -> []
    dropExport l = if "export " `isPrefixOf` l then drop 7 l else l
    strip = dropWhileEnd isSpace . dropWhile isSpace
    unquote v = case v of
      q : rest | q `elem` ("\"'" :: String), not (null rest), last rest == q -> init rest
      _ -> v