package envutil import ( "os" "path/filepath" "strings" "github.com/gookit/goutil/internal/comfunc" ) // DefaultEnvFile default file name const DefaultEnvFile = ".env" // Dotenv load and parse dotenv files type Dotenv struct { // Files dot env file paths, allow multi files. // - filename support simple glob pattern. eg: ".env.*" // // default: [".env"] Files []string // BaseDir base dir for join files path // // default is workdir BaseDir string // UpperKey change key to upper on set ENV. default: true UpperKey bool // IgnoreNotExist only load exists. // // - default: false - will return error if not exists IgnoreNotExist bool // LoadFirstExist only load first exists env file on Files LoadFirstExist bool loadFiles []string loadData map[string]string } // NewDotenv create a new dotenv config func NewDotenv() *Dotenv { return &Dotenv{ UpperKey: true, Files: []string{DefaultEnvFile}, // init fields loadData: make(map[string]string), } } // LoadAndInit load dotenv files and parse to os.Environ func (c *Dotenv) LoadAndInit() error { return c.doLoadFiles(c.Files) } // LoadFiles append load dotenv files // - filename support simple glob pattern. eg: ".env.*" func (c *Dotenv) LoadFiles(files ...string) error { return c.doLoadFiles(files) } // LoadText load dotenv contents and parse to os Env func (c *Dotenv) LoadText(contents string) error { return c.parseAndSetEnv(contents) } // do load dotenv files func (c *Dotenv) doLoadFiles(files []string) error { var filePath string for _, file := range files { filePath = strings.TrimSpace(file) if c.BaseDir != "" && !filepath.IsAbs(file) { filePath = filepath.Join(c.BaseDir, file) } // load and parse to ENV if err := c.loadFile(filePath); err != nil { return err } if c.LoadFirstExist && len(c.loadFiles) > 0 { break } } return nil } func (c *Dotenv) loadFile(filePath string) error { // filename support simple glob pattern. if strings.ContainsRune(filePath, '*') { matches, err := filepath.Glob(filePath) if err != nil { return err } for _, matchFile := range matches { if err = c.parseFile(matchFile); err != nil { return err } } return err } // Load single file return c.parseFile(filePath) } // parseFile load single file and parse to ENV func (c *Dotenv) parseFile(filePath string) error { contents, err := os.ReadFile(filePath) if err != nil { // IgnoreNotExist: skip non-existent files if c.IgnoreNotExist && os.IsNotExist(err) { return nil } return err } err = c.parseAndSetEnv(string(contents)) if err == nil { c.loadFiles = append(c.loadFiles, filePath) } return err } func (c *Dotenv) parseAndSetEnv(contents string) error { // Parse ENV lines envMp, err := comfunc.ParseEnvLines(contents, comfunc.ParseEnvLineOption{ SkipOnErrorLine: true, }) // Set to ENV for key, val := range envMp { key = strings.ToUpper(key) c.loadData[key] = val _ = os.Setenv(key, val) } return err } // UnloadEnv remove loaded dotenv data from os.Environ func (c *Dotenv) UnloadEnv() bool { if len(c.loadData) == 0 { return false } for key := range c.loadData { _ = os.Unsetenv(key) } return true } // LoadedData get loaded dotenv data map func (c *Dotenv) LoadedData() map[string]string { return c.loadData } // LoadedFiles get loaded dotenv files func (c *Dotenv) LoadedFiles() []string { return c.loadFiles } // Reset unload all loaded ENV and reset data func (c *Dotenv) Reset() { c.UnloadEnv() c.loadFiles = nil c.loadData = make(map[string]string) } // // region standard dotenv instance // var stdEnv = NewDotenv() // StdDotenv get standard dotenv instance func StdDotenv() *Dotenv { return stdEnv } // DotenvLoad load dotenv file and parse to ENV func DotenvLoad(fns ...func(cfg *Dotenv)) error { for _, fn := range fns { fn(stdEnv) } return stdEnv.LoadAndInit() } // LoadEnvFiles load dotenv files and parse to ENV func LoadEnvFiles(baseDir string, files ...string) error { return DotenvLoad(func(cfg *Dotenv) { cfg.BaseDir = baseDir if len(files) > 0 { cfg.Files = files } }) } // LoadedEnvFiles get loaded dotenv files func LoadedEnvFiles() []string { return stdEnv.LoadedFiles() }