Merged in feature/serviceconfig (pull request #33)
DRAFT PR: Prototype for serviceconfig * in progress * in progress * extract common methods * Merge remote-tracking branch 'origin/feature/serviceconfig' into feature/serviceconfig * fix types * cleanup * add tests * Merged main into feature/serviceconfig Approved-by: Michael McGuinness
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"queryorchestration/internal/serviceconfig"
|
||||
)
|
||||
|
||||
// Note: All that a service needs to have its own config is to create a new
|
||||
// config type (any name you want) and add your customer variables to it that
|
||||
// then call the serviceconfig.InitializeConfig() function on it.
|
||||
type AnyServiceNameConfig struct {
|
||||
// BaseConfig has all of the common base configuration that all services should have
|
||||
// we can adjust what is in there as needed.
|
||||
serviceconfig.BaseConfig // Embed the base configuration
|
||||
|
||||
// add any custom values that this service needs
|
||||
AppEnv string `env:"APP_ENV"`
|
||||
}
|
||||
|
||||
// main
|
||||
// Dead simple example of of how to initialize a service custom config - AnyServiceNameConfig object in any project code.
|
||||
func main() {
|
||||
|
||||
// create your own customer service config
|
||||
cfg := &AnyServiceNameConfig{}
|
||||
// and let the shared code initialize it for you correctly.
|
||||
if err := serviceconfig.InitializeConfig(cfg); err != nil {
|
||||
fmt.Printf("Error initializing the custom config (see output): %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("config: %+v\n", cfg)
|
||||
cfg.Logger.Info("Service initialized ok.")
|
||||
|
||||
}
|
||||
@@ -6,10 +6,12 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.5
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.5
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.37.1
|
||||
github.com/caarlos0/env/v11 v11.3.1
|
||||
github.com/getkin/kin-openapi v0.128.0
|
||||
github.com/go-playground/validator/v10 v10.23.0
|
||||
github.com/golang-migrate/migrate/v4 v4.18.1
|
||||
github.com/jackc/pgx/v5 v5.7.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/labstack/echo/v4 v4.13.3
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/oapi-codegen/runtime v1.1.1
|
||||
|
||||
@@ -44,6 +44,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyN
|
||||
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
|
||||
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
@@ -135,6 +137,8 @@ github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
|
||||
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
package serviceconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/caarlos0/env/v11"
|
||||
"github.com/joho/godotenv"
|
||||
"log/slog"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Common Types
|
||||
// ------------
|
||||
|
||||
// DbConfig contains database connection configuration parameters
|
||||
// that are shared across services.
|
||||
type DbConfig struct {
|
||||
DBUser string `env:"DB_USER,required,notEmpty"` // Database username
|
||||
DBSecret string `env:"DB_PASS,required,notEmpty"` // Database password
|
||||
DBHost string `env:"DB_HOST,required,notEmpty"` // Database host address
|
||||
DBPort int `env:"DB_PORT,required,notEmpty"` // Database port number
|
||||
DBName string `env:"DB_NAME,required,notEmpty"` // Database name
|
||||
DBNoSSL bool `env:"DB_NOSSL" envDefault:"false"` // SSL mode configuration
|
||||
}
|
||||
|
||||
// LoggerConfig wraps the application logger instance.
|
||||
type LoggerConfig struct {
|
||||
Logger *slog.Logger // Structured logger instance
|
||||
}
|
||||
|
||||
// AwsConfig contains AWS credentials and configuration.
|
||||
type AwsConfig struct {
|
||||
AWSClientID string `env:"AWS_CLIENT_ID"` // AWS client identifier
|
||||
AWSSecretKey string `env:"AWS_SECRET_KEY"` // AWS secret access key
|
||||
}
|
||||
|
||||
// BaseConfig provides common configuration fields and functionality
|
||||
// that can be embedded in service-specific configs.
|
||||
type BaseConfig struct {
|
||||
LoggerConfig
|
||||
DbConfig
|
||||
AwsConfig
|
||||
}
|
||||
|
||||
// Interfaces
|
||||
// ----------
|
||||
|
||||
// ConfigProvider defines the basic requirements for all configuration types.
|
||||
// Implementations must provide logging capabilities and configuration display.
|
||||
type ConfigProvider interface {
|
||||
GetLogger() *slog.Logger
|
||||
LogConfig(prefixSecret string)
|
||||
}
|
||||
|
||||
// Configuration Methods
|
||||
// -------------------
|
||||
|
||||
// InitializeConfig sets up a configuration instance by loading environment
|
||||
// variables and initializing the logger. It expects a ConfigProvider implementation.
|
||||
// Returns an error if initialization fails.
|
||||
func InitializeConfig(cfg ConfigProvider) error {
|
||||
// First, initialize the logger by getting the base config through reflection
|
||||
val := reflect.ValueOf(cfg).Elem()
|
||||
|
||||
// Look for the embedded BaseConfig
|
||||
var baseConfig *BaseConfig
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
if val.Type().Field(i).Anonymous && val.Field(i).Type() == reflect.TypeOf(BaseConfig{}) {
|
||||
baseConfig = val.Field(i).Addr().Interface().(*BaseConfig)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if baseConfig == nil {
|
||||
return fmt.Errorf("no BaseConfig found in the provided config struct")
|
||||
}
|
||||
|
||||
// Initialize the logger in the base config
|
||||
baseConfig.Logger = slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
slog.SetDefault(baseConfig.Logger)
|
||||
|
||||
// Load .env file
|
||||
if err := godotenv.Load(); err != nil {
|
||||
slog.Warn("No .env file found or error loading it", "error", err)
|
||||
}
|
||||
|
||||
// Parse environment variables
|
||||
if err := env.Parse(cfg); err != nil {
|
||||
if envErr, ok := err.(env.AggregateError); ok {
|
||||
slog.Error("Missing required environment variables:")
|
||||
for _, e := range envErr.Errors {
|
||||
slog.Error(e.Error())
|
||||
}
|
||||
} else {
|
||||
slog.Error("Error parsing environment variables", "error", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Log the configuration
|
||||
cfg.LogConfig("secret")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLogger implements the ConfigProvider interface by returning
|
||||
// the configured logger instance.
|
||||
func (b *BaseConfig) GetLogger() *slog.Logger {
|
||||
return b.Logger
|
||||
}
|
||||
|
||||
// Logging Methods
|
||||
// --------------
|
||||
|
||||
// LogConfig displays the current configuration values, masking sensitive
|
||||
// information based on the provided prefixSecret. It implements the
|
||||
// ConfigProvider interface.
|
||||
func (b *BaseConfig) LogConfig(prefixSecret string) {
|
||||
b.logConfigRecursive(reflect.ValueOf(b), "", prefixSecret, make(map[reflect.Value]bool))
|
||||
}
|
||||
|
||||
// logConfigRecursive is a helper method that recursively traverses the configuration
|
||||
// structure and logs each field's value. It handles nested structs, pointers,
|
||||
// and masks sensitive values containing the specified prefixSecret in their names.
|
||||
func (b *BaseConfig) logConfigRecursive(val reflect.Value, prefix string, prefixSecret string, visited map[reflect.Value]bool) {
|
||||
// Handle pointer dereference
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
// Prevent infinite recursion
|
||||
if visited[val] {
|
||||
return
|
||||
}
|
||||
visited[val] = true
|
||||
|
||||
typ := val.Type()
|
||||
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
fieldType := typ.Field(i)
|
||||
|
||||
// Skip unexported fields
|
||||
if !fieldType.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldName := fieldType.Name
|
||||
fullPath := prefix + fieldName
|
||||
|
||||
// Handle embedded fields
|
||||
if fieldType.Anonymous {
|
||||
b.logConfigRecursive(field, prefix, prefixSecret, visited)
|
||||
continue
|
||||
}
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.Struct:
|
||||
b.logConfigRecursive(field, fullPath+".", prefixSecret, visited)
|
||||
default:
|
||||
var valueStr string
|
||||
if field.Kind() == reflect.String {
|
||||
valueStr = field.String()
|
||||
} else {
|
||||
valueStr = fmt.Sprintf("%v", field.Interface())
|
||||
}
|
||||
|
||||
// Mask sensitive values
|
||||
if strings.Contains(strings.ToLower(fieldName), strings.ToLower(prefixSecret)) {
|
||||
if len(valueStr) > 5 {
|
||||
valueStr = valueStr[:5] + "..."
|
||||
}
|
||||
}
|
||||
|
||||
b.Logger.Info("Config value",
|
||||
"key", fullPath,
|
||||
"value", valueStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# Service Configuration
|
||||
This directory contains the service configuration for the DoczyAI project.
|
||||
Common code that is shared by all of the services in the project.
|
||||
|
||||
Note that the library currently supports only mapping ENV (and .env) variables to the root of your
|
||||
configuration struct type. Mapping is by name. See `https://github.com/caarlos0/env` for rules around tagging.
|
||||
|
||||
For example:
|
||||
```
|
||||
type AnyServiceNameConfig struct {
|
||||
// BaseConfig has all of the common base configuration that all services should have.
|
||||
serviceconfig.BaseConfig // Embed the base configuration
|
||||
|
||||
// add any custom values that this service needs
|
||||
SomeCustomStringValue string `env:"SOME_CUSTOM_STRING_VALUE"`
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,126 @@
|
||||
package serviceconfig
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type TestingServiceNameConfig struct {
|
||||
BaseConfig
|
||||
AppEnv string `env:"APP_ENV"`
|
||||
BoolTest bool `env:"BOOL_TEST"`
|
||||
IntTest int `env:"INT_TEST,required,notEmpty"`
|
||||
}
|
||||
|
||||
func TestInitializeConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
envVars map[string]string
|
||||
wantErr bool
|
||||
errMessageContains string
|
||||
}{
|
||||
{
|
||||
name: "valid configuration",
|
||||
envVars: map[string]string{
|
||||
"APP_ENV": "testing",
|
||||
"BOOL_TEST": "true",
|
||||
"INT_TEST": "42",
|
||||
"DB_USER": "postgres",
|
||||
"DB_PASS": "pass",
|
||||
"DB_HOST": "localhost",
|
||||
"DB_PORT": "5432",
|
||||
"DB_NAME": "query_orchestration",
|
||||
"DB_NOSSL": "true",
|
||||
"SUB_FIELD1:": "value1",
|
||||
"SUB_FIELD2": "42",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing required env var",
|
||||
envVars: map[string]string{
|
||||
"APP_ENV": "testing",
|
||||
"BOOL_TEST": "true",
|
||||
"DB_USER": "postgres",
|
||||
"DB_PASS": "pass",
|
||||
"DB_HOST": "localhost",
|
||||
"DB_PORT": "5432",
|
||||
"DB_NAME": "query_orchestration",
|
||||
"DB_NOSSL": "true",
|
||||
// INT_TEST intentionally omitted
|
||||
},
|
||||
wantErr: true,
|
||||
errMessageContains: "INT_TEST",
|
||||
},
|
||||
{
|
||||
name: "invalid boolean value",
|
||||
envVars: map[string]string{
|
||||
"APP_ENV": "testing",
|
||||
"BOOL_TEST": "notabool",
|
||||
"INT_TEST": "42",
|
||||
},
|
||||
wantErr: true,
|
||||
errMessageContains: "BoolTest",
|
||||
},
|
||||
{
|
||||
name: "invalid integer value",
|
||||
envVars: map[string]string{
|
||||
"APP_ENV": "testing",
|
||||
"BOOL_TEST": "true",
|
||||
"INT_TEST": "notanint",
|
||||
},
|
||||
wantErr: true,
|
||||
errMessageContains: "parse error on field \"IntTest\"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Clear environment before each test
|
||||
os.Clearenv()
|
||||
|
||||
// Set environment variables for the test
|
||||
for k, v := range tt.envVars {
|
||||
os.Setenv(k, v)
|
||||
}
|
||||
|
||||
cfg := &TestingServiceNameConfig{}
|
||||
err := InitializeConfig(cfg)
|
||||
|
||||
logger := cfg.GetLogger()
|
||||
assert.NotNil(t, logger)
|
||||
logger.Info("Logger initialized")
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("InitializeConfig() error = nil, wantErr %v", tt.wantErr)
|
||||
return
|
||||
}
|
||||
// if we expect an error, check if the error message tt.errMessageContains is contained in the error message
|
||||
if tt.errMessageContains != "" && !strings.Contains(err.Error(), tt.errMessageContains) {
|
||||
t.Errorf("InitializeConfig() error = %v, want error containing %v", err, tt.errMessageContains)
|
||||
}
|
||||
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("InitializeConfig() unexpected error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the values were set correctly
|
||||
if cfg.AppEnv != tt.envVars["APP_ENV"] {
|
||||
t.Errorf("AppEnv = %v, want %v", cfg.AppEnv, tt.envVars["APP_ENV"])
|
||||
}
|
||||
if cfg.BoolTest != (tt.envVars["BOOL_TEST"] == "true") {
|
||||
t.Errorf("BoolTest = %v, want %v", cfg.BoolTest, tt.envVars["BOOL_TEST"] == "true")
|
||||
}
|
||||
expectedInt := 42 // Known value from test cases
|
||||
if cfg.IntTest != expectedInt {
|
||||
t.Errorf("IntTest = %v, want %v", cfg.IntTest, expectedInt)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
max_line_length = 120
|
||||
|
||||
[{go.mod,go.sum,*.go}]
|
||||
insert_final_newline = true
|
||||
indent_size = tab
|
||||
indent_style = tab
|
||||
tab_width = 4
|
||||
|
||||
[Makefile]
|
||||
max_line_length = off
|
||||
insert_final_newline = true
|
||||
indent_size = tab
|
||||
indent_style = tab
|
||||
tab_width = 4
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
indent_size = tab
|
||||
indent_style = space
|
||||
tab_width = 2
|
||||
|
||||
[.mailmap]
|
||||
max_line_length = off
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
coverage.txt
|
||||
bin
|
||||
card.png
|
||||
dist
|
||||
codecov*
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
linters-settings:
|
||||
gocritic:
|
||||
enabled-checks:
|
||||
- emptyStringTest
|
||||
- evalOrder
|
||||
- paramTypeCombine
|
||||
- preferStringWriter
|
||||
- sprintfQuotedString
|
||||
- stringConcatSimplify
|
||||
- yodaStyleExpr
|
||||
revive:
|
||||
rules:
|
||||
- name: line-length-limit
|
||||
arguments: [120]
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- revive
|
||||
text: "line-length-limit:"
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- thelper
|
||||
- gofumpt
|
||||
- gocritic
|
||||
- tparallel
|
||||
- unconvert
|
||||
- unparam
|
||||
- wastedassign
|
||||
- revive
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||
version: 2
|
||||
includes:
|
||||
- from_url:
|
||||
url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/lib.yml
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Carlos A Becker <caarlos0@gmail.com>
|
||||
Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Carlos A Becker <caarlos0@users.noreply.github.com>
|
||||
Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Carlos Alexandro Becker <caarlos0@gmail.com>
|
||||
Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
|
||||
Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Carlos Becker <caarlos0@gmail.com>
|
||||
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
|
||||
actions-user <actions@github.com> github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2024 Carlos Alexandro Becker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
SOURCE_FILES?=./...
|
||||
TEST_PATTERN?=.
|
||||
|
||||
export GO111MODULE := on
|
||||
|
||||
setup:
|
||||
go mod tidy
|
||||
.PHONY: setup
|
||||
|
||||
build:
|
||||
go build
|
||||
.PHONY: build
|
||||
|
||||
test:
|
||||
go test -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m
|
||||
.PHONY: test
|
||||
|
||||
cover: test
|
||||
go tool cover -html=coverage.txt
|
||||
.PHONY: cover
|
||||
|
||||
fmt:
|
||||
gofumpt -w -l .
|
||||
.PHONY: fmt
|
||||
|
||||
lint:
|
||||
golangci-lint run ./...
|
||||
.PHONY: lint
|
||||
|
||||
ci: build test
|
||||
.PHONY: ci
|
||||
|
||||
card:
|
||||
wget -O card.png -c "https://og.caarlos0.dev/**env**: parse envs to structs.png?theme=light&md=1&fontSize=100px&images=https://github.com/caarlos0.png"
|
||||
.PHONY: card
|
||||
|
||||
.DEFAULT_GOAL := ci
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
<p align="center">
|
||||
<img alt="GoReleaser Logo" src="https://becker.software/env.png" height="140" />
|
||||
<p align="center">A simple, zero-dependencies library to parse environment variables into structs.</p>
|
||||
</p>
|
||||
|
||||
###### Installation
|
||||
|
||||
```bash
|
||||
go get github.com/caarlos0/env/v11
|
||||
```
|
||||
|
||||
###### Getting started
|
||||
|
||||
```go
|
||||
type config struct {
|
||||
Home string `env:"HOME"`
|
||||
}
|
||||
|
||||
// parse
|
||||
var cfg config
|
||||
err := env.Parse(&cfg)
|
||||
|
||||
// parse with generics
|
||||
cfg, err := env.ParseAs[config]()
|
||||
```
|
||||
|
||||
You can see the full documentation and list of examples at [pkg.go.dev](https://pkg.go.dev/github.com/caarlos0/env/v11).
|
||||
|
||||
---
|
||||
|
||||
## Used and supported by
|
||||
|
||||
<p>
|
||||
<a href="https://encore.dev">
|
||||
<img src="https://user-images.githubusercontent.com/78424526/214602214-52e0483a-b5fc-4d4c-b03e-0b7b23e012df.svg" width="120px" alt="encore icon" />
|
||||
</a>
|
||||
<br/>
|
||||
<br/>
|
||||
<b>Encore – the platform for building Go-based cloud backends.</b>
|
||||
<br/>
|
||||
</p>
|
||||
|
||||
## Usage
|
||||
|
||||
### Caveats
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> _Unexported fields_ will be **ignored** by `env`.
|
||||
> This is by design and will not change.
|
||||
|
||||
### Functions
|
||||
|
||||
- `Parse`: parse the current environment into a type
|
||||
- `ParseAs`: parse the current environment into a type using generics
|
||||
- `ParseWithOptions`: parse the current environment into a type with custom options
|
||||
- `ParseAsWithOptions`: parse the current environment into a type with custom options and using generics
|
||||
- `Must`: can be used to wrap `Parse.*` calls to panic on error
|
||||
- `GetFieldParams`: get the `env` parsed options for a type
|
||||
- `GetFieldParamsWithOptions`: get the `env` parsed options for a type with custom options
|
||||
|
||||
### Supported types
|
||||
|
||||
Out of the box all built-in types are supported, plus a few others that are commonly used.
|
||||
|
||||
Complete list:
|
||||
|
||||
- `bool`
|
||||
- `float32`
|
||||
- `float64`
|
||||
- `int16`
|
||||
- `int32`
|
||||
- `int64`
|
||||
- `int8`
|
||||
- `int`
|
||||
- `string`
|
||||
- `uint16`
|
||||
- `uint32`
|
||||
- `uint64`
|
||||
- `uint8`
|
||||
- `uint`
|
||||
- `time.Duration`
|
||||
- `time.Location`
|
||||
- `encoding.TextUnmarshaler`
|
||||
- `url.URL`
|
||||
|
||||
Pointers, slices and slices of pointers, and maps of those types are also supported.
|
||||
|
||||
You may also add custom parsers for your types.
|
||||
|
||||
### Tags
|
||||
|
||||
The following tags are provided:
|
||||
|
||||
- `env`: sets the environment variable name and optionally takes the tag options described below
|
||||
- `envDefault`: sets the default value for the field
|
||||
- `envPrefix`: can be used in a field that is a complex type to set a prefix to all environment variables used in it
|
||||
- `envSeparator`: sets the character to be used to separate items in slices and maps (default: `,`)
|
||||
- `envKeyValSeparator`: sets the character to be used to separate keys and their values in maps (default: `:`)
|
||||
|
||||
### `env` tag options
|
||||
|
||||
Here are all the options available for the `env` tag:
|
||||
|
||||
- `,expand`: expands environment variables, e.g. `FOO_${BAR}`
|
||||
- `,file`: instructs that the content of the variable is a path to a file that should be read
|
||||
- `,init`: initialize nil pointers
|
||||
- `,notEmpty`: make the field errors if the environment variable is empty
|
||||
- `,required`: make the field errors if the environment variable is not set
|
||||
- `,unset`: unset the environment variable after use
|
||||
|
||||
### Parse Options
|
||||
|
||||
There are a few options available in the functions that end with `WithOptions`:
|
||||
|
||||
- `Environment`: keys and values to be used instead of `os.Environ()`
|
||||
- `TagName`: specifies another tag name to use rather than the default `env`
|
||||
- `PrefixTagName`: specifies another prefix tag name to use rather than the default `envPrefix`
|
||||
- `DefaultValueTagName`: specifies another default tag name to use rather than the default `envDefault`
|
||||
- `RequiredIfNoDef`: set all `env` fields as required if they do not declare `envDefault`
|
||||
- `OnSet`: allows to hook into the `env` parsing and do something when a value is set
|
||||
- `Prefix`: prefix to be used in all environment variables
|
||||
- `UseFieldNameByDefault`: defines whether or not `env` should use the field name by default if the `env` key is missing
|
||||
- `FuncMap`: custom parse functions for custom types
|
||||
|
||||
### Documentation and examples
|
||||
|
||||
Examples are live in [pkg.go.dev](https://pkg.go.dev/github.com/caarlos0/env/v11),
|
||||
and also in the [example test file](./example_test.go).
|
||||
|
||||
## Current state
|
||||
|
||||
`env` is considered feature-complete.
|
||||
|
||||
I do not intent to add any new features unless they really make sense, and are
|
||||
requested by many people.
|
||||
|
||||
Eventual bug fixes will keep being merged.
|
||||
|
||||
## Badges
|
||||
|
||||
[](https://github.com/goreleaser/goreleaser/releases/latest)
|
||||
[](/LICENSE.md)
|
||||
[](https://github.com/caarlos0/env/actions?workflow=build)
|
||||
[](https://codecov.io/gh/caarlos0/env)
|
||||
[](http://godoc.org/github.com/caarlos0/env/v11)
|
||||
[](https://github.com/goreleaser)
|
||||
[](https://conventionalcommits.org)
|
||||
|
||||
## Related projects
|
||||
|
||||
- [envdoc](https://github.com/g4s8/envdoc) - generate documentation for environment variables from `env` tags
|
||||
|
||||
## Stargazers over time
|
||||
|
||||
[](https://starchart.cc/caarlos0/env)
|
||||
+845
@@ -0,0 +1,845 @@
|
||||
// Package env is a simple, zero-dependencies library to parse environment
|
||||
// variables into structs.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type config struct {
|
||||
// Home string `env:"HOME"`
|
||||
// }
|
||||
// // parse
|
||||
// var cfg config
|
||||
// err := env.Parse(&cfg)
|
||||
// // or parse with generics
|
||||
// cfg, err := env.ParseAs[config]()
|
||||
//
|
||||
// Check the examples and README for more detailed usage.
|
||||
package env
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// nolint: gochecknoglobals
|
||||
var (
|
||||
defaultBuiltInParsers = map[reflect.Kind]ParserFunc{
|
||||
reflect.Bool: func(v string) (interface{}, error) {
|
||||
return strconv.ParseBool(v)
|
||||
},
|
||||
reflect.String: func(v string) (interface{}, error) {
|
||||
return v, nil
|
||||
},
|
||||
reflect.Int: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseInt(v, 10, 32)
|
||||
return int(i), err
|
||||
},
|
||||
reflect.Int16: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseInt(v, 10, 16)
|
||||
return int16(i), err
|
||||
},
|
||||
reflect.Int32: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseInt(v, 10, 32)
|
||||
return int32(i), err
|
||||
},
|
||||
reflect.Int64: func(v string) (interface{}, error) {
|
||||
return strconv.ParseInt(v, 10, 64)
|
||||
},
|
||||
reflect.Int8: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseInt(v, 10, 8)
|
||||
return int8(i), err
|
||||
},
|
||||
reflect.Uint: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseUint(v, 10, 32)
|
||||
return uint(i), err
|
||||
},
|
||||
reflect.Uint16: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseUint(v, 10, 16)
|
||||
return uint16(i), err
|
||||
},
|
||||
reflect.Uint32: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseUint(v, 10, 32)
|
||||
return uint32(i), err
|
||||
},
|
||||
reflect.Uint64: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseUint(v, 10, 64)
|
||||
return i, err
|
||||
},
|
||||
reflect.Uint8: func(v string) (interface{}, error) {
|
||||
i, err := strconv.ParseUint(v, 10, 8)
|
||||
return uint8(i), err
|
||||
},
|
||||
reflect.Float64: func(v string) (interface{}, error) {
|
||||
return strconv.ParseFloat(v, 64)
|
||||
},
|
||||
reflect.Float32: func(v string) (interface{}, error) {
|
||||
f, err := strconv.ParseFloat(v, 32)
|
||||
return float32(f), err
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func defaultTypeParsers() map[reflect.Type]ParserFunc {
|
||||
return map[reflect.Type]ParserFunc{
|
||||
reflect.TypeOf(url.URL{}): parseURL,
|
||||
reflect.TypeOf(time.Nanosecond): parseDuration,
|
||||
reflect.TypeOf(time.Location{}): parseLocation,
|
||||
}
|
||||
}
|
||||
|
||||
func parseURL(v string) (interface{}, error) {
|
||||
u, err := url.Parse(v)
|
||||
if err != nil {
|
||||
return nil, newParseValueError("unable to parse URL", err)
|
||||
}
|
||||
return *u, nil
|
||||
}
|
||||
|
||||
func parseDuration(v string) (interface{}, error) {
|
||||
d, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return nil, newParseValueError("unable to parse duration", err)
|
||||
}
|
||||
return d, err
|
||||
}
|
||||
|
||||
func parseLocation(v string) (interface{}, error) {
|
||||
loc, err := time.LoadLocation(v)
|
||||
if err != nil {
|
||||
return nil, newParseValueError("unable to parse location", err)
|
||||
}
|
||||
return *loc, nil
|
||||
}
|
||||
|
||||
// ParserFunc defines the signature of a function that can be used within
|
||||
// `Options`' `FuncMap`.
|
||||
type ParserFunc func(v string) (interface{}, error)
|
||||
|
||||
// OnSetFn is a hook that can be run when a value is set.
|
||||
type OnSetFn func(tag string, value interface{}, isDefault bool)
|
||||
|
||||
// processFieldFn is a function which takes all information about a field and processes it.
|
||||
type processFieldFn func(
|
||||
refField reflect.Value,
|
||||
refTypeField reflect.StructField,
|
||||
opts Options,
|
||||
fieldParams FieldParams,
|
||||
) error
|
||||
|
||||
// Options for the parser.
|
||||
type Options struct {
|
||||
// Environment keys and values that will be accessible for the service.
|
||||
Environment map[string]string
|
||||
|
||||
// TagName specifies another tag name to use rather than the default 'env'.
|
||||
TagName string
|
||||
|
||||
// PrefixTagName specifies another prefix tag name to use rather than the default 'envPrefix'.
|
||||
PrefixTagName string
|
||||
|
||||
// DefaultValueTagName specifies another default tag name to use rather than the default 'envDefault'.
|
||||
DefaultValueTagName string
|
||||
|
||||
// RequiredIfNoDef automatically sets all fields as required if they do not
|
||||
// declare 'envDefault'.
|
||||
RequiredIfNoDef bool
|
||||
|
||||
// OnSet allows to run a function when a value is set.
|
||||
OnSet OnSetFn
|
||||
|
||||
// Prefix define a prefix for every key.
|
||||
Prefix string
|
||||
|
||||
// UseFieldNameByDefault defines whether or not `env` should use the field
|
||||
// name by default if the `env` key is missing.
|
||||
// Note that the field name will be "converted" to conform with environment
|
||||
// variable names conventions.
|
||||
UseFieldNameByDefault bool
|
||||
|
||||
// Custom parse functions for different types.
|
||||
FuncMap map[reflect.Type]ParserFunc
|
||||
|
||||
// Used internally. maps the env variable key to its resolved string value.
|
||||
// (for env var expansion)
|
||||
rawEnvVars map[string]string
|
||||
}
|
||||
|
||||
func (opts *Options) getRawEnv(s string) string {
|
||||
val := opts.rawEnvVars[s]
|
||||
if val == "" {
|
||||
val = opts.Environment[s]
|
||||
}
|
||||
return os.Expand(val, opts.getRawEnv)
|
||||
}
|
||||
|
||||
func defaultOptions() Options {
|
||||
return Options{
|
||||
TagName: "env",
|
||||
PrefixTagName: "envPrefix",
|
||||
DefaultValueTagName: "envDefault",
|
||||
Environment: toMap(os.Environ()),
|
||||
FuncMap: defaultTypeParsers(),
|
||||
rawEnvVars: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func mergeOptions[T any](target, source *T) {
|
||||
targetPtr := reflect.ValueOf(target).Elem()
|
||||
sourcePtr := reflect.ValueOf(source).Elem()
|
||||
|
||||
targetType := targetPtr.Type()
|
||||
for i := 0; i < targetPtr.NumField(); i++ {
|
||||
fieldName := targetType.Field(i).Name
|
||||
targetField := targetPtr.Field(i)
|
||||
sourceField := sourcePtr.FieldByName(fieldName)
|
||||
|
||||
if targetField.CanSet() && !isZero(sourceField) {
|
||||
// FuncMaps are being merged, while Environments must be overwritten
|
||||
if fieldName == "FuncMap" {
|
||||
if !sourceField.IsZero() {
|
||||
iter := sourceField.MapRange()
|
||||
for iter.Next() {
|
||||
targetField.SetMapIndex(iter.Key(), iter.Value())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
targetField.Set(sourceField)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isZero(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.Func, reflect.Map, reflect.Slice:
|
||||
return v.IsNil()
|
||||
default:
|
||||
zero := reflect.Zero(v.Type())
|
||||
return v.Interface() == zero.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
func customOptions(opts Options) Options {
|
||||
defOpts := defaultOptions()
|
||||
mergeOptions(&defOpts, &opts)
|
||||
return defOpts
|
||||
}
|
||||
|
||||
func optionsWithSliceEnvPrefix(opts Options, index int) Options {
|
||||
return Options{
|
||||
Environment: opts.Environment,
|
||||
TagName: opts.TagName,
|
||||
PrefixTagName: opts.PrefixTagName,
|
||||
DefaultValueTagName: opts.DefaultValueTagName,
|
||||
RequiredIfNoDef: opts.RequiredIfNoDef,
|
||||
OnSet: opts.OnSet,
|
||||
Prefix: fmt.Sprintf("%s%d_", opts.Prefix, index),
|
||||
UseFieldNameByDefault: opts.UseFieldNameByDefault,
|
||||
FuncMap: opts.FuncMap,
|
||||
rawEnvVars: opts.rawEnvVars,
|
||||
}
|
||||
}
|
||||
|
||||
func optionsWithEnvPrefix(field reflect.StructField, opts Options) Options {
|
||||
return Options{
|
||||
Environment: opts.Environment,
|
||||
TagName: opts.TagName,
|
||||
PrefixTagName: opts.PrefixTagName,
|
||||
DefaultValueTagName: opts.DefaultValueTagName,
|
||||
RequiredIfNoDef: opts.RequiredIfNoDef,
|
||||
OnSet: opts.OnSet,
|
||||
Prefix: opts.Prefix + field.Tag.Get(opts.PrefixTagName),
|
||||
UseFieldNameByDefault: opts.UseFieldNameByDefault,
|
||||
FuncMap: opts.FuncMap,
|
||||
rawEnvVars: opts.rawEnvVars,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses a struct containing `env` tags and loads its values from
|
||||
// environment variables.
|
||||
func Parse(v interface{}) error {
|
||||
return parseInternal(v, setField, defaultOptions())
|
||||
}
|
||||
|
||||
// ParseWithOptions parses a struct containing `env` tags and loads its values from
|
||||
// environment variables.
|
||||
func ParseWithOptions(v interface{}, opts Options) error {
|
||||
return parseInternal(v, setField, customOptions(opts))
|
||||
}
|
||||
|
||||
// ParseAs parses the given struct type containing `env` tags and loads its
|
||||
// values from environment variables.
|
||||
func ParseAs[T any]() (T, error) {
|
||||
var t T
|
||||
err := Parse(&t)
|
||||
return t, err
|
||||
}
|
||||
|
||||
// ParseWithOptions parses the given struct type containing `env` tags and
|
||||
// loads its values from environment variables.
|
||||
func ParseAsWithOptions[T any](opts Options) (T, error) {
|
||||
var t T
|
||||
err := ParseWithOptions(&t, opts)
|
||||
return t, err
|
||||
}
|
||||
|
||||
// Must panic is if err is not nil, and returns t otherwise.
|
||||
func Must[T any](t T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// GetFieldParams parses a struct containing `env` tags and returns information about
|
||||
// tags it found.
|
||||
func GetFieldParams(v interface{}) ([]FieldParams, error) {
|
||||
return GetFieldParamsWithOptions(v, defaultOptions())
|
||||
}
|
||||
|
||||
// GetFieldParamsWithOptions parses a struct containing `env` tags and returns information about
|
||||
// tags it found.
|
||||
func GetFieldParamsWithOptions(v interface{}, opts Options) ([]FieldParams, error) {
|
||||
var result []FieldParams
|
||||
err := parseInternal(
|
||||
v,
|
||||
func(_ reflect.Value, _ reflect.StructField, _ Options, fieldParams FieldParams) error {
|
||||
if fieldParams.OwnKey != "" {
|
||||
result = append(result, fieldParams)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
customOptions(opts),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseInternal(v interface{}, processField processFieldFn, opts Options) error {
|
||||
ptrRef := reflect.ValueOf(v)
|
||||
if ptrRef.Kind() != reflect.Ptr {
|
||||
return newAggregateError(NotStructPtrError{})
|
||||
}
|
||||
ref := ptrRef.Elem()
|
||||
if ref.Kind() != reflect.Struct {
|
||||
return newAggregateError(NotStructPtrError{})
|
||||
}
|
||||
|
||||
return doParse(ref, processField, opts)
|
||||
}
|
||||
|
||||
func doParse(ref reflect.Value, processField processFieldFn, opts Options) error {
|
||||
refType := ref.Type()
|
||||
|
||||
var agrErr AggregateError
|
||||
|
||||
for i := 0; i < refType.NumField(); i++ {
|
||||
refField := ref.Field(i)
|
||||
refTypeField := refType.Field(i)
|
||||
|
||||
if err := doParseField(refField, refTypeField, processField, opts); err != nil {
|
||||
if val, ok := err.(AggregateError); ok {
|
||||
agrErr.Errors = append(agrErr.Errors, val.Errors...)
|
||||
} else {
|
||||
agrErr.Errors = append(agrErr.Errors, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(agrErr.Errors) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return agrErr
|
||||
}
|
||||
|
||||
func doParseField(
|
||||
refField reflect.Value,
|
||||
refTypeField reflect.StructField,
|
||||
processField processFieldFn,
|
||||
opts Options,
|
||||
) error {
|
||||
if !refField.CanSet() {
|
||||
return nil
|
||||
}
|
||||
if refField.Kind() == reflect.Ptr && refField.Elem().Kind() == reflect.Struct && !refField.IsNil() {
|
||||
return parseInternal(refField.Interface(), processField, optionsWithEnvPrefix(refTypeField, opts))
|
||||
}
|
||||
if refField.Kind() == reflect.Struct && refField.CanAddr() && refField.Type().Name() == "" {
|
||||
return parseInternal(refField.Addr().Interface(), processField, optionsWithEnvPrefix(refTypeField, opts))
|
||||
}
|
||||
|
||||
params, err := parseFieldParams(refTypeField, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if params.Ignored {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := processField(refField, refTypeField, opts, params); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if params.Init && isInvalidPtr(refField) {
|
||||
refField.Set(reflect.New(refField.Type().Elem()))
|
||||
refField = refField.Elem()
|
||||
}
|
||||
|
||||
if refField.Kind() == reflect.Struct {
|
||||
return doParse(refField, processField, optionsWithEnvPrefix(refTypeField, opts))
|
||||
}
|
||||
|
||||
if isSliceOfStructs(refTypeField) {
|
||||
return doParseSlice(refField, processField, optionsWithEnvPrefix(refTypeField, opts))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isSliceOfStructs(refTypeField reflect.StructField) bool {
|
||||
field := refTypeField.Type
|
||||
|
||||
// *[]struct
|
||||
if field.Kind() == reflect.Ptr {
|
||||
field = field.Elem()
|
||||
if field.Kind() == reflect.Slice && field.Elem().Kind() == reflect.Struct {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// []struct{}
|
||||
if field.Kind() == reflect.Slice && field.Elem().Kind() == reflect.Struct {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func doParseSlice(ref reflect.Value, processField processFieldFn, opts Options) error {
|
||||
if opts.Prefix != "" && !strings.HasSuffix(opts.Prefix, string(underscore)) {
|
||||
opts.Prefix += string(underscore)
|
||||
}
|
||||
|
||||
var environments []string
|
||||
for environment := range opts.Environment {
|
||||
if strings.HasPrefix(environment, opts.Prefix) {
|
||||
environments = append(environments, environment)
|
||||
}
|
||||
}
|
||||
|
||||
if len(environments) > 0 {
|
||||
counter := 0
|
||||
for finished := false; !finished; {
|
||||
finished = true
|
||||
prefix := fmt.Sprintf("%s%d%c", opts.Prefix, counter, underscore)
|
||||
for _, variable := range environments {
|
||||
if strings.HasPrefix(variable, prefix) {
|
||||
counter++
|
||||
finished = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sliceType := ref.Type()
|
||||
var initialized int
|
||||
if reflect.Ptr == ref.Kind() {
|
||||
sliceType = sliceType.Elem()
|
||||
// Due to the rest of code the pre-initialized slice has no chance for this situation
|
||||
initialized = 0
|
||||
} else {
|
||||
initialized = ref.Len()
|
||||
}
|
||||
|
||||
var capacity int
|
||||
if capacity = initialized; counter > initialized {
|
||||
capacity = counter
|
||||
}
|
||||
result := reflect.MakeSlice(sliceType, capacity, capacity)
|
||||
for i := 0; i < capacity; i++ {
|
||||
item := result.Index(i)
|
||||
if i < initialized {
|
||||
item.Set(ref.Index(i))
|
||||
}
|
||||
if err := doParse(item, processField, optionsWithSliceEnvPrefix(opts, i)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if result.Len() > 0 {
|
||||
if reflect.Ptr == ref.Kind() {
|
||||
resultPtr := reflect.New(sliceType)
|
||||
resultPtr.Elem().Set(result)
|
||||
result = resultPtr
|
||||
}
|
||||
ref.Set(result)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setField(refField reflect.Value, refTypeField reflect.StructField, opts Options, fieldParams FieldParams) error {
|
||||
value, err := get(fieldParams, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if value != "" {
|
||||
return set(refField, refTypeField, value, opts.FuncMap)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const underscore rune = '_'
|
||||
|
||||
func toEnvName(input string) string {
|
||||
var output []rune
|
||||
for i, c := range input {
|
||||
if c == underscore {
|
||||
continue
|
||||
}
|
||||
if len(output) > 0 && unicode.IsUpper(c) {
|
||||
if len(input) > i+1 {
|
||||
peek := rune(input[i+1])
|
||||
if unicode.IsLower(peek) || unicode.IsLower(rune(input[i-1])) {
|
||||
output = append(output, underscore)
|
||||
}
|
||||
}
|
||||
}
|
||||
output = append(output, unicode.ToUpper(c))
|
||||
}
|
||||
return string(output)
|
||||
}
|
||||
|
||||
// FieldParams contains information about parsed field tags.
|
||||
type FieldParams struct {
|
||||
OwnKey string
|
||||
Key string
|
||||
DefaultValue string
|
||||
HasDefaultValue bool
|
||||
Required bool
|
||||
LoadFile bool
|
||||
Unset bool
|
||||
NotEmpty bool
|
||||
Expand bool
|
||||
Init bool
|
||||
Ignored bool
|
||||
}
|
||||
|
||||
func parseFieldParams(field reflect.StructField, opts Options) (FieldParams, error) {
|
||||
ownKey, tags := parseKeyForOption(field.Tag.Get(opts.TagName))
|
||||
if ownKey == "" && opts.UseFieldNameByDefault {
|
||||
ownKey = toEnvName(field.Name)
|
||||
}
|
||||
|
||||
defaultValue, hasDefaultValue := field.Tag.Lookup(opts.DefaultValueTagName)
|
||||
|
||||
result := FieldParams{
|
||||
OwnKey: ownKey,
|
||||
Key: opts.Prefix + ownKey,
|
||||
Required: opts.RequiredIfNoDef,
|
||||
DefaultValue: defaultValue,
|
||||
HasDefaultValue: hasDefaultValue,
|
||||
Ignored: ownKey == "-",
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
switch tag {
|
||||
case "":
|
||||
continue
|
||||
case "file":
|
||||
result.LoadFile = true
|
||||
case "required":
|
||||
result.Required = true
|
||||
case "unset":
|
||||
result.Unset = true
|
||||
case "notEmpty":
|
||||
result.NotEmpty = true
|
||||
case "expand":
|
||||
result.Expand = true
|
||||
case "init":
|
||||
result.Init = true
|
||||
case "-":
|
||||
result.Ignored = true
|
||||
default:
|
||||
return FieldParams{}, newNoSupportedTagOptionError(tag)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func get(fieldParams FieldParams, opts Options) (val string, err error) {
|
||||
var exists, isDefault bool
|
||||
|
||||
val, exists, isDefault = getOr(
|
||||
fieldParams.Key,
|
||||
fieldParams.DefaultValue,
|
||||
fieldParams.HasDefaultValue,
|
||||
opts.Environment,
|
||||
)
|
||||
|
||||
if fieldParams.Expand {
|
||||
val = os.Expand(val, opts.getRawEnv)
|
||||
}
|
||||
|
||||
opts.rawEnvVars[fieldParams.OwnKey] = val
|
||||
|
||||
if fieldParams.Unset {
|
||||
defer os.Unsetenv(fieldParams.Key)
|
||||
}
|
||||
|
||||
if fieldParams.Required && !exists && fieldParams.OwnKey != "" {
|
||||
return "", newVarIsNotSetError(fieldParams.Key)
|
||||
}
|
||||
|
||||
if fieldParams.NotEmpty && val == "" {
|
||||
return "", newEmptyVarError(fieldParams.Key)
|
||||
}
|
||||
|
||||
if fieldParams.LoadFile && val != "" {
|
||||
filename := val
|
||||
val, err = getFromFile(filename)
|
||||
if err != nil {
|
||||
return "", newLoadFileContentError(filename, fieldParams.Key, err)
|
||||
}
|
||||
}
|
||||
|
||||
if opts.OnSet != nil {
|
||||
if fieldParams.OwnKey != "" {
|
||||
opts.OnSet(fieldParams.Key, val, isDefault)
|
||||
}
|
||||
}
|
||||
return val, err
|
||||
}
|
||||
|
||||
// split the env tag's key into the expected key and desired option, if any.
|
||||
func parseKeyForOption(key string) (string, []string) {
|
||||
opts := strings.Split(key, ",")
|
||||
return opts[0], opts[1:]
|
||||
}
|
||||
|
||||
func getFromFile(filename string) (value string, err error) {
|
||||
b, err := os.ReadFile(filename)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
func getOr(key, defaultValue string, defExists bool, envs map[string]string) (val string, exists, isDefault bool) {
|
||||
value, exists := envs[key]
|
||||
switch {
|
||||
case (!exists || key == "") && defExists:
|
||||
return defaultValue, true, true
|
||||
case exists && value == "" && defExists:
|
||||
return defaultValue, true, true
|
||||
case !exists:
|
||||
return "", false, false
|
||||
}
|
||||
|
||||
return value, true, false
|
||||
}
|
||||
|
||||
func set(field reflect.Value, sf reflect.StructField, value string, funcMap map[reflect.Type]ParserFunc) error {
|
||||
if tm := asTextUnmarshaler(field); tm != nil {
|
||||
if err := tm.UnmarshalText([]byte(value)); err != nil {
|
||||
return newParseError(sf, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
typee := sf.Type
|
||||
fieldee := field
|
||||
if typee.Kind() == reflect.Ptr {
|
||||
typee = typee.Elem()
|
||||
fieldee = field.Elem()
|
||||
}
|
||||
|
||||
parserFunc, ok := funcMap[typee]
|
||||
if ok {
|
||||
val, err := parserFunc(value)
|
||||
if err != nil {
|
||||
return newParseError(sf, err)
|
||||
}
|
||||
|
||||
fieldee.Set(reflect.ValueOf(val))
|
||||
return nil
|
||||
}
|
||||
|
||||
parserFunc, ok = defaultBuiltInParsers[typee.Kind()]
|
||||
if ok {
|
||||
val, err := parserFunc(value)
|
||||
if err != nil {
|
||||
return newParseError(sf, err)
|
||||
}
|
||||
|
||||
fieldee.Set(reflect.ValueOf(val).Convert(typee))
|
||||
return nil
|
||||
}
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.Slice:
|
||||
return handleSlice(field, value, sf, funcMap)
|
||||
case reflect.Map:
|
||||
return handleMap(field, value, sf, funcMap)
|
||||
}
|
||||
|
||||
return newNoParserError(sf)
|
||||
}
|
||||
|
||||
func handleSlice(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error {
|
||||
separator := sf.Tag.Get("envSeparator")
|
||||
if separator == "" {
|
||||
separator = ","
|
||||
}
|
||||
parts := strings.Split(value, separator)
|
||||
|
||||
typee := sf.Type.Elem()
|
||||
if typee.Kind() == reflect.Ptr {
|
||||
typee = typee.Elem()
|
||||
}
|
||||
|
||||
if _, ok := reflect.New(typee).Interface().(encoding.TextUnmarshaler); ok {
|
||||
return parseTextUnmarshalers(field, parts, sf)
|
||||
}
|
||||
|
||||
parserFunc, ok := funcMap[typee]
|
||||
if !ok {
|
||||
parserFunc, ok = defaultBuiltInParsers[typee.Kind()]
|
||||
if !ok {
|
||||
return newNoParserError(sf)
|
||||
}
|
||||
}
|
||||
|
||||
result := reflect.MakeSlice(sf.Type, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
r, err := parserFunc(part)
|
||||
if err != nil {
|
||||
return newParseError(sf, err)
|
||||
}
|
||||
v := reflect.ValueOf(r).Convert(typee)
|
||||
if sf.Type.Elem().Kind() == reflect.Ptr {
|
||||
v = reflect.New(typee)
|
||||
v.Elem().Set(reflect.ValueOf(r).Convert(typee))
|
||||
}
|
||||
result = reflect.Append(result, v)
|
||||
}
|
||||
field.Set(result)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMap(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error {
|
||||
keyType := sf.Type.Key()
|
||||
keyParserFunc, ok := funcMap[keyType]
|
||||
if !ok {
|
||||
keyParserFunc, ok = defaultBuiltInParsers[keyType.Kind()]
|
||||
if !ok {
|
||||
return newNoParserError(sf)
|
||||
}
|
||||
}
|
||||
|
||||
elemType := sf.Type.Elem()
|
||||
elemParserFunc, ok := funcMap[elemType]
|
||||
if !ok {
|
||||
elemParserFunc, ok = defaultBuiltInParsers[elemType.Kind()]
|
||||
if !ok {
|
||||
return newNoParserError(sf)
|
||||
}
|
||||
}
|
||||
|
||||
separator := sf.Tag.Get("envSeparator")
|
||||
if separator == "" {
|
||||
separator = ","
|
||||
}
|
||||
|
||||
keyValSeparator := sf.Tag.Get("envKeyValSeparator")
|
||||
if keyValSeparator == "" {
|
||||
keyValSeparator = ":"
|
||||
}
|
||||
|
||||
result := reflect.MakeMap(sf.Type)
|
||||
for _, part := range strings.Split(value, separator) {
|
||||
pairs := strings.SplitN(part, keyValSeparator, 2)
|
||||
if len(pairs) != 2 {
|
||||
return newParseError(sf, fmt.Errorf(`%q should be in "key%svalue" format`, part, keyValSeparator))
|
||||
}
|
||||
|
||||
key, err := keyParserFunc(pairs[0])
|
||||
if err != nil {
|
||||
return newParseError(sf, err)
|
||||
}
|
||||
|
||||
elem, err := elemParserFunc(pairs[1])
|
||||
if err != nil {
|
||||
return newParseError(sf, err)
|
||||
}
|
||||
|
||||
result.SetMapIndex(reflect.ValueOf(key).Convert(keyType), reflect.ValueOf(elem).Convert(elemType))
|
||||
}
|
||||
|
||||
field.Set(result)
|
||||
return nil
|
||||
}
|
||||
|
||||
func asTextUnmarshaler(field reflect.Value) encoding.TextUnmarshaler {
|
||||
if field.Kind() == reflect.Ptr {
|
||||
if field.IsNil() {
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
}
|
||||
} else if field.CanAddr() {
|
||||
field = field.Addr()
|
||||
}
|
||||
|
||||
tm, ok := field.Interface().(encoding.TextUnmarshaler)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return tm
|
||||
}
|
||||
|
||||
func parseTextUnmarshalers(field reflect.Value, data []string, sf reflect.StructField) error {
|
||||
s := len(data)
|
||||
elemType := field.Type().Elem()
|
||||
slice := reflect.MakeSlice(reflect.SliceOf(elemType), s, s)
|
||||
for i, v := range data {
|
||||
sv := slice.Index(i)
|
||||
kind := sv.Kind()
|
||||
if kind == reflect.Ptr {
|
||||
sv = reflect.New(elemType.Elem())
|
||||
} else {
|
||||
sv = sv.Addr()
|
||||
}
|
||||
tm := sv.Interface().(encoding.TextUnmarshaler)
|
||||
if err := tm.UnmarshalText([]byte(v)); err != nil {
|
||||
return newParseError(sf, err)
|
||||
}
|
||||
if kind == reflect.Ptr {
|
||||
slice.Index(i).Set(sv)
|
||||
}
|
||||
}
|
||||
|
||||
field.Set(slice)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToMap Converts list of env vars as provided by os.Environ() to map you
|
||||
// can use as Options.Environment field
|
||||
func ToMap(env []string) map[string]string {
|
||||
return toMap(env)
|
||||
}
|
||||
|
||||
func isInvalidPtr(v reflect.Value) bool {
|
||||
return reflect.Ptr == v.Kind() && v.Elem().Kind() == reflect.Invalid
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
//go:build !windows
|
||||
|
||||
package env
|
||||
|
||||
import "strings"
|
||||
|
||||
func toMap(env []string) map[string]string {
|
||||
r := map[string]string{}
|
||||
for _, e := range env {
|
||||
p := strings.SplitN(e, "=", 2)
|
||||
if len(p) == 2 {
|
||||
r[p[0]] = p[1]
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
//go:build windows
|
||||
|
||||
package env
|
||||
|
||||
import "strings"
|
||||
|
||||
func toMap(env []string) map[string]string {
|
||||
r := map[string]string{}
|
||||
for _, e := range env {
|
||||
p := strings.SplitN(e, "=", 2)
|
||||
|
||||
// On Windows, environment variables can start with '='. If so, Split at next character.
|
||||
// See env_windows.go in the Go source: https://github.com/golang/go/blob/master/src/syscall/env_windows.go#L58
|
||||
prefixEqualSign := false
|
||||
if len(e) > 0 && e[0] == '=' {
|
||||
e = e[1:]
|
||||
prefixEqualSign = true
|
||||
}
|
||||
p = strings.SplitN(e, "=", 2)
|
||||
if prefixEqualSign {
|
||||
p[0] = "=" + p[0]
|
||||
}
|
||||
|
||||
if len(p) == 2 {
|
||||
r[p[0]] = p[1]
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// An aggregated error wrapper to combine gathered errors.
|
||||
// This allows either to display all errors or convert them individually
|
||||
// List of the available errors
|
||||
// ParseError
|
||||
// NotStructPtrError
|
||||
// NoParserError
|
||||
// NoSupportedTagOptionError
|
||||
// VarIsNotSetError
|
||||
// EmptyVarError
|
||||
// LoadFileContentError
|
||||
// ParseValueError
|
||||
type AggregateError struct {
|
||||
Errors []error
|
||||
}
|
||||
|
||||
func newAggregateError(initErr error) error {
|
||||
return AggregateError{
|
||||
[]error{
|
||||
initErr,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (e AggregateError) Error() string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString("env:")
|
||||
|
||||
for _, err := range e.Errors {
|
||||
sb.WriteString(fmt.Sprintf(" %v;", err.Error()))
|
||||
}
|
||||
|
||||
return strings.TrimRight(sb.String(), ";")
|
||||
}
|
||||
|
||||
// Unwrap implements std errors.Join go1.20 compatibility
|
||||
func (e AggregateError) Unwrap() []error {
|
||||
return e.Errors
|
||||
}
|
||||
|
||||
// Is conforms with errors.Is.
|
||||
func (e AggregateError) Is(err error) bool {
|
||||
for _, ie := range e.Errors {
|
||||
if reflect.TypeOf(ie) == reflect.TypeOf(err) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// The error occurs when it's impossible to convert the value for given type.
|
||||
type ParseError struct {
|
||||
Name string
|
||||
Type reflect.Type
|
||||
Err error
|
||||
}
|
||||
|
||||
func newParseError(sf reflect.StructField, err error) error {
|
||||
return ParseError{sf.Name, sf.Type, err}
|
||||
}
|
||||
|
||||
func (e ParseError) Error() string {
|
||||
return fmt.Sprintf("parse error on field %q of type %q: %v", e.Name, e.Type, e.Err)
|
||||
}
|
||||
|
||||
// The error occurs when pass something that is not a pointer to a struct to Parse
|
||||
type NotStructPtrError struct{}
|
||||
|
||||
func (e NotStructPtrError) Error() string {
|
||||
return "expected a pointer to a Struct"
|
||||
}
|
||||
|
||||
// This error occurs when there is no parser provided for given type.
|
||||
type NoParserError struct {
|
||||
Name string
|
||||
Type reflect.Type
|
||||
}
|
||||
|
||||
func newNoParserError(sf reflect.StructField) error {
|
||||
return NoParserError{sf.Name, sf.Type}
|
||||
}
|
||||
|
||||
func (e NoParserError) Error() string {
|
||||
return fmt.Sprintf("no parser found for field %q of type %q", e.Name, e.Type)
|
||||
}
|
||||
|
||||
// This error occurs when the given tag is not supported.
|
||||
// Built-in supported tags: "", "file", "required", "unset", "notEmpty",
|
||||
// "expand", "envDefault", and "envSeparator".
|
||||
type NoSupportedTagOptionError struct {
|
||||
Tag string
|
||||
}
|
||||
|
||||
func newNoSupportedTagOptionError(tag string) error {
|
||||
return NoSupportedTagOptionError{tag}
|
||||
}
|
||||
|
||||
func (e NoSupportedTagOptionError) Error() string {
|
||||
return fmt.Sprintf("tag option %q not supported", e.Tag)
|
||||
}
|
||||
|
||||
// This error occurs when the required variable is not set.
|
||||
//
|
||||
// Deprecated: use VarIsNotSetError.
|
||||
type EnvVarIsNotSetError = VarIsNotSetError
|
||||
|
||||
// This error occurs when the required variable is not set.
|
||||
type VarIsNotSetError struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func newVarIsNotSetError(key string) error {
|
||||
return VarIsNotSetError{key}
|
||||
}
|
||||
|
||||
func (e VarIsNotSetError) Error() string {
|
||||
return fmt.Sprintf(`required environment variable %q is not set`, e.Key)
|
||||
}
|
||||
|
||||
// This error occurs when the variable which must be not empty is existing but has an empty value
|
||||
//
|
||||
// Deprecated: use EmptyVarError.
|
||||
type EmptyEnvVarError = EmptyVarError
|
||||
|
||||
// This error occurs when the variable which must be not empty is existing but has an empty value
|
||||
type EmptyVarError struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func newEmptyVarError(key string) error {
|
||||
return EmptyVarError{key}
|
||||
}
|
||||
|
||||
func (e EmptyVarError) Error() string {
|
||||
return fmt.Sprintf("environment variable %q should not be empty", e.Key)
|
||||
}
|
||||
|
||||
// This error occurs when it's impossible to load the value from the file.
|
||||
type LoadFileContentError struct {
|
||||
Filename string
|
||||
Key string
|
||||
Err error
|
||||
}
|
||||
|
||||
func newLoadFileContentError(filename, key string, err error) error {
|
||||
return LoadFileContentError{filename, key, err}
|
||||
}
|
||||
|
||||
func (e LoadFileContentError) Error() string {
|
||||
return fmt.Sprintf("could not load content of file %q from variable %s: %v", e.Filename, e.Key, e.Err)
|
||||
}
|
||||
|
||||
// This error occurs when it's impossible to convert value using given parser.
|
||||
type ParseValueError struct {
|
||||
Msg string
|
||||
Err error
|
||||
}
|
||||
|
||||
func newParseValueError(message string, err error) error {
|
||||
return ParseValueError{message, err}
|
||||
}
|
||||
|
||||
func (e ParseValueError) Error() string {
|
||||
return fmt.Sprintf("%s: %v", e.Msg, e.Err)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
.DS_Store
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
Copyright (c) 2013 John Barton
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
# GoDotEnv  [](https://goreportcard.com/report/github.com/joho/godotenv)
|
||||
|
||||
A Go (golang) port of the Ruby [dotenv](https://github.com/bkeepers/dotenv) project (which loads env vars from a .env file).
|
||||
|
||||
From the original Library:
|
||||
|
||||
> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables.
|
||||
>
|
||||
> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped.
|
||||
|
||||
It can be used as a library (for loading in env for your own daemons etc.) or as a bin command.
|
||||
|
||||
There is test coverage and CI for both linuxish and Windows environments, but I make no guarantees about the bin version working on Windows.
|
||||
|
||||
## Installation
|
||||
|
||||
As a library
|
||||
|
||||
```shell
|
||||
go get github.com/joho/godotenv
|
||||
```
|
||||
|
||||
or if you want to use it as a bin command
|
||||
|
||||
go >= 1.17
|
||||
```shell
|
||||
go install github.com/joho/godotenv/cmd/godotenv@latest
|
||||
```
|
||||
|
||||
go < 1.17
|
||||
```shell
|
||||
go get github.com/joho/godotenv/cmd/godotenv
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Add your application configuration to your `.env` file in the root of your project:
|
||||
|
||||
```shell
|
||||
S3_BUCKET=YOURS3BUCKET
|
||||
SECRET_KEY=YOURSECRETKEYGOESHERE
|
||||
```
|
||||
|
||||
Then in your Go app you can do something like
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Fatal("Error loading .env file")
|
||||
}
|
||||
|
||||
s3Bucket := os.Getenv("S3_BUCKET")
|
||||
secretKey := os.Getenv("SECRET_KEY")
|
||||
|
||||
// now do something with s3 or whatever
|
||||
}
|
||||
```
|
||||
|
||||
If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import
|
||||
|
||||
```go
|
||||
import _ "github.com/joho/godotenv/autoload"
|
||||
```
|
||||
|
||||
While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit
|
||||
|
||||
```go
|
||||
godotenv.Load("somerandomfile")
|
||||
godotenv.Load("filenumberone.env", "filenumbertwo.env")
|
||||
```
|
||||
|
||||
If you want to be really fancy with your env file you can do comments and exports (below is a valid env file)
|
||||
|
||||
```shell
|
||||
# I am a comment and that is OK
|
||||
SOME_VAR=someval
|
||||
FOO=BAR # comments at line end are OK too
|
||||
export BAR=BAZ
|
||||
```
|
||||
|
||||
Or finally you can do YAML(ish) style
|
||||
|
||||
```yaml
|
||||
FOO: bar
|
||||
BAR: baz
|
||||
```
|
||||
|
||||
as a final aside, if you don't want godotenv munging your env you can just get a map back instead
|
||||
|
||||
```go
|
||||
var myEnv map[string]string
|
||||
myEnv, err := godotenv.Read()
|
||||
|
||||
s3Bucket := myEnv["S3_BUCKET"]
|
||||
```
|
||||
|
||||
... or from an `io.Reader` instead of a local file
|
||||
|
||||
```go
|
||||
reader := getRemoteFile()
|
||||
myEnv, err := godotenv.Parse(reader)
|
||||
```
|
||||
|
||||
... or from a `string` if you so desire
|
||||
|
||||
```go
|
||||
content := getRemoteFileContent()
|
||||
myEnv, err := godotenv.Unmarshal(content)
|
||||
```
|
||||
|
||||
### Precedence & Conventions
|
||||
|
||||
Existing envs take precedence of envs that are loaded later.
|
||||
|
||||
The [convention](https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use)
|
||||
for managing multiple environments (i.e. development, test, production)
|
||||
is to create an env named `{YOURAPP}_ENV` and load envs in this order:
|
||||
|
||||
```go
|
||||
env := os.Getenv("FOO_ENV")
|
||||
if "" == env {
|
||||
env = "development"
|
||||
}
|
||||
|
||||
godotenv.Load(".env." + env + ".local")
|
||||
if "test" != env {
|
||||
godotenv.Load(".env.local")
|
||||
}
|
||||
godotenv.Load(".env." + env)
|
||||
godotenv.Load() // The Original .env
|
||||
```
|
||||
|
||||
If you need to, you can also use `godotenv.Overload()` to defy this convention
|
||||
and overwrite existing envs instead of only supplanting them. Use with caution.
|
||||
|
||||
### Command Mode
|
||||
|
||||
Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH`
|
||||
|
||||
```
|
||||
godotenv -f /some/path/to/.env some_command with some args
|
||||
```
|
||||
|
||||
If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD`
|
||||
|
||||
By default, it won't override existing environment variables; you can do that with the `-o` flag.
|
||||
|
||||
### Writing Env Files
|
||||
|
||||
Godotenv can also write a map representing the environment to a correctly-formatted and escaped file
|
||||
|
||||
```go
|
||||
env, err := godotenv.Unmarshal("KEY=value")
|
||||
err := godotenv.Write(env, "./.env")
|
||||
```
|
||||
|
||||
... or to a string
|
||||
|
||||
```go
|
||||
env, err := godotenv.Unmarshal("KEY=value")
|
||||
content, err := godotenv.Marshal(env)
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome, but with some caveats.
|
||||
|
||||
This library has been declared feature complete (see [#182](https://github.com/joho/godotenv/issues/182) for background) and will not be accepting issues or pull requests adding new functionality or breaking the library API.
|
||||
|
||||
Contributions would be gladly accepted that:
|
||||
|
||||
* bring this library's parsing into closer compatibility with the mainline dotenv implementations, in particular [Ruby's dotenv](https://github.com/bkeepers/dotenv) and [Node.js' dotenv](https://github.com/motdotla/dotenv)
|
||||
* keep the library up to date with the go ecosystem (ie CI bumps, documentation changes, changes in the core libraries)
|
||||
* bug fixes for use cases that pertain to the library's purpose of easing development of codebases deployed into twelve factor environments
|
||||
|
||||
*code changes without tests and references to peer dotenv implementations will not be accepted*
|
||||
|
||||
1. Fork it
|
||||
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||
3. Commit your changes (`git commit -am 'Added some feature'`)
|
||||
4. Push to the branch (`git push origin my-new-feature`)
|
||||
5. Create new Pull Request
|
||||
|
||||
## Releases
|
||||
|
||||
Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`.
|
||||
|
||||
Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1`
|
||||
|
||||
## Who?
|
||||
|
||||
The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library.
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv)
|
||||
//
|
||||
// Examples/readme can be found on the GitHub page at https://github.com/joho/godotenv
|
||||
//
|
||||
// The TL;DR is that you make a .env file that looks something like
|
||||
//
|
||||
// SOME_ENV_VAR=somevalue
|
||||
//
|
||||
// and then in your go code you can call
|
||||
//
|
||||
// godotenv.Load()
|
||||
//
|
||||
// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR")
|
||||
package godotenv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const doubleQuoteSpecialChars = "\\\n\r\"!$`"
|
||||
|
||||
// Parse reads an env file from io.Reader, returning a map of keys and values.
|
||||
func Parse(r io.Reader) (map[string]string, error) {
|
||||
var buf bytes.Buffer
|
||||
_, err := io.Copy(&buf, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return UnmarshalBytes(buf.Bytes())
|
||||
}
|
||||
|
||||
// Load will read your env file(s) and load them into ENV for this process.
|
||||
//
|
||||
// Call this function as close as possible to the start of your program (ideally in main).
|
||||
//
|
||||
// If you call Load without any args it will default to loading .env in the current path.
|
||||
//
|
||||
// You can otherwise tell it which files to load (there can be more than one) like:
|
||||
//
|
||||
// godotenv.Load("fileone", "filetwo")
|
||||
//
|
||||
// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults.
|
||||
func Load(filenames ...string) (err error) {
|
||||
filenames = filenamesOrDefault(filenames)
|
||||
|
||||
for _, filename := range filenames {
|
||||
err = loadFile(filename, false)
|
||||
if err != nil {
|
||||
return // return early on a spazout
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Overload will read your env file(s) and load them into ENV for this process.
|
||||
//
|
||||
// Call this function as close as possible to the start of your program (ideally in main).
|
||||
//
|
||||
// If you call Overload without any args it will default to loading .env in the current path.
|
||||
//
|
||||
// You can otherwise tell it which files to load (there can be more than one) like:
|
||||
//
|
||||
// godotenv.Overload("fileone", "filetwo")
|
||||
//
|
||||
// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefully set all vars.
|
||||
func Overload(filenames ...string) (err error) {
|
||||
filenames = filenamesOrDefault(filenames)
|
||||
|
||||
for _, filename := range filenames {
|
||||
err = loadFile(filename, true)
|
||||
if err != nil {
|
||||
return // return early on a spazout
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Read all env (with same file loading semantics as Load) but return values as
|
||||
// a map rather than automatically writing values into env
|
||||
func Read(filenames ...string) (envMap map[string]string, err error) {
|
||||
filenames = filenamesOrDefault(filenames)
|
||||
envMap = make(map[string]string)
|
||||
|
||||
for _, filename := range filenames {
|
||||
individualEnvMap, individualErr := readFile(filename)
|
||||
|
||||
if individualErr != nil {
|
||||
err = individualErr
|
||||
return // return early on a spazout
|
||||
}
|
||||
|
||||
for key, value := range individualEnvMap {
|
||||
envMap[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Unmarshal reads an env file from a string, returning a map of keys and values.
|
||||
func Unmarshal(str string) (envMap map[string]string, err error) {
|
||||
return UnmarshalBytes([]byte(str))
|
||||
}
|
||||
|
||||
// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values.
|
||||
func UnmarshalBytes(src []byte) (map[string]string, error) {
|
||||
out := make(map[string]string)
|
||||
err := parseBytes(src, out)
|
||||
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Exec loads env vars from the specified filenames (empty map falls back to default)
|
||||
// then executes the cmd specified.
|
||||
//
|
||||
// Simply hooks up os.Stdin/err/out to the command and calls Run().
|
||||
//
|
||||
// If you want more fine grained control over your command it's recommended
|
||||
// that you use `Load()`, `Overload()` or `Read()` and the `os/exec` package yourself.
|
||||
func Exec(filenames []string, cmd string, cmdArgs []string, overload bool) error {
|
||||
op := Load
|
||||
if overload {
|
||||
op = Overload
|
||||
}
|
||||
if err := op(filenames...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
command := exec.Command(cmd, cmdArgs...)
|
||||
command.Stdin = os.Stdin
|
||||
command.Stdout = os.Stdout
|
||||
command.Stderr = os.Stderr
|
||||
return command.Run()
|
||||
}
|
||||
|
||||
// Write serializes the given environment and writes it to a file.
|
||||
func Write(envMap map[string]string, filename string) error {
|
||||
content, err := Marshal(envMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = file.WriteString(content + "\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return file.Sync()
|
||||
}
|
||||
|
||||
// Marshal outputs the given environment as a dotenv-formatted environment file.
|
||||
// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped.
|
||||
func Marshal(envMap map[string]string) (string, error) {
|
||||
lines := make([]string, 0, len(envMap))
|
||||
for k, v := range envMap {
|
||||
if d, err := strconv.Atoi(v); err == nil {
|
||||
lines = append(lines, fmt.Sprintf(`%s=%d`, k, d))
|
||||
} else {
|
||||
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v)))
|
||||
}
|
||||
}
|
||||
sort.Strings(lines)
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
func filenamesOrDefault(filenames []string) []string {
|
||||
if len(filenames) == 0 {
|
||||
return []string{".env"}
|
||||
}
|
||||
return filenames
|
||||
}
|
||||
|
||||
func loadFile(filename string, overload bool) error {
|
||||
envMap, err := readFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentEnv := map[string]bool{}
|
||||
rawEnv := os.Environ()
|
||||
for _, rawEnvLine := range rawEnv {
|
||||
key := strings.Split(rawEnvLine, "=")[0]
|
||||
currentEnv[key] = true
|
||||
}
|
||||
|
||||
for key, value := range envMap {
|
||||
if !currentEnv[key] || overload {
|
||||
_ = os.Setenv(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readFile(filename string) (envMap map[string]string, err error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return Parse(file)
|
||||
}
|
||||
|
||||
func doubleQuoteEscape(line string) string {
|
||||
for _, c := range doubleQuoteSpecialChars {
|
||||
toReplace := "\\" + string(c)
|
||||
if c == '\n' {
|
||||
toReplace = `\n`
|
||||
}
|
||||
if c == '\r' {
|
||||
toReplace = `\r`
|
||||
}
|
||||
line = strings.Replace(line, string(c), toReplace, -1)
|
||||
}
|
||||
return line
|
||||
}
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
package godotenv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
charComment = '#'
|
||||
prefixSingleQuote = '\''
|
||||
prefixDoubleQuote = '"'
|
||||
|
||||
exportPrefix = "export"
|
||||
)
|
||||
|
||||
func parseBytes(src []byte, out map[string]string) error {
|
||||
src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1)
|
||||
cutset := src
|
||||
for {
|
||||
cutset = getStatementStart(cutset)
|
||||
if cutset == nil {
|
||||
// reached end of file
|
||||
break
|
||||
}
|
||||
|
||||
key, left, err := locateKeyName(cutset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
value, left, err := extractVarValue(left, out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out[key] = value
|
||||
cutset = left
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getStatementPosition returns position of statement begin.
|
||||
//
|
||||
// It skips any comment line or non-whitespace character.
|
||||
func getStatementStart(src []byte) []byte {
|
||||
pos := indexOfNonSpaceChar(src)
|
||||
if pos == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
src = src[pos:]
|
||||
if src[0] != charComment {
|
||||
return src
|
||||
}
|
||||
|
||||
// skip comment section
|
||||
pos = bytes.IndexFunc(src, isCharFunc('\n'))
|
||||
if pos == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return getStatementStart(src[pos:])
|
||||
}
|
||||
|
||||
// locateKeyName locates and parses key name and returns rest of slice
|
||||
func locateKeyName(src []byte) (key string, cutset []byte, err error) {
|
||||
// trim "export" and space at beginning
|
||||
src = bytes.TrimLeftFunc(src, isSpace)
|
||||
if bytes.HasPrefix(src, []byte(exportPrefix)) {
|
||||
trimmed := bytes.TrimPrefix(src, []byte(exportPrefix))
|
||||
if bytes.IndexFunc(trimmed, isSpace) == 0 {
|
||||
src = bytes.TrimLeftFunc(trimmed, isSpace)
|
||||
}
|
||||
}
|
||||
|
||||
// locate key name end and validate it in single loop
|
||||
offset := 0
|
||||
loop:
|
||||
for i, char := range src {
|
||||
rchar := rune(char)
|
||||
if isSpace(rchar) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch char {
|
||||
case '=', ':':
|
||||
// library also supports yaml-style value declaration
|
||||
key = string(src[0:i])
|
||||
offset = i + 1
|
||||
break loop
|
||||
case '_':
|
||||
default:
|
||||
// variable name should match [A-Za-z0-9_.]
|
||||
if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' {
|
||||
continue
|
||||
}
|
||||
|
||||
return "", nil, fmt.Errorf(
|
||||
`unexpected character %q in variable name near %q`,
|
||||
string(char), string(src))
|
||||
}
|
||||
}
|
||||
|
||||
if len(src) == 0 {
|
||||
return "", nil, errors.New("zero length string")
|
||||
}
|
||||
|
||||
// trim whitespace
|
||||
key = strings.TrimRightFunc(key, unicode.IsSpace)
|
||||
cutset = bytes.TrimLeftFunc(src[offset:], isSpace)
|
||||
return key, cutset, nil
|
||||
}
|
||||
|
||||
// extractVarValue extracts variable value and returns rest of slice
|
||||
func extractVarValue(src []byte, vars map[string]string) (value string, rest []byte, err error) {
|
||||
quote, hasPrefix := hasQuotePrefix(src)
|
||||
if !hasPrefix {
|
||||
// unquoted value - read until end of line
|
||||
endOfLine := bytes.IndexFunc(src, isLineEnd)
|
||||
|
||||
// Hit EOF without a trailing newline
|
||||
if endOfLine == -1 {
|
||||
endOfLine = len(src)
|
||||
|
||||
if endOfLine == 0 {
|
||||
return "", nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Convert line to rune away to do accurate countback of runes
|
||||
line := []rune(string(src[0:endOfLine]))
|
||||
|
||||
// Assume end of line is end of var
|
||||
endOfVar := len(line)
|
||||
if endOfVar == 0 {
|
||||
return "", src[endOfLine:], nil
|
||||
}
|
||||
|
||||
// Work backwards to check if the line ends in whitespace then
|
||||
// a comment (ie asdasd # some comment)
|
||||
for i := endOfVar - 1; i >= 0; i-- {
|
||||
if line[i] == charComment && i > 0 {
|
||||
if isSpace(line[i-1]) {
|
||||
endOfVar = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace)
|
||||
|
||||
return expandVariables(trimmed, vars), src[endOfLine:], nil
|
||||
}
|
||||
|
||||
// lookup quoted string terminator
|
||||
for i := 1; i < len(src); i++ {
|
||||
if char := src[i]; char != quote {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip escaped quote symbol (\" or \', depends on quote)
|
||||
if prevChar := src[i-1]; prevChar == '\\' {
|
||||
continue
|
||||
}
|
||||
|
||||
// trim quotes
|
||||
trimFunc := isCharFunc(rune(quote))
|
||||
value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc))
|
||||
if quote == prefixDoubleQuote {
|
||||
// unescape newlines for double quote (this is compat feature)
|
||||
// and expand environment variables
|
||||
value = expandVariables(expandEscapes(value), vars)
|
||||
}
|
||||
|
||||
return value, src[i+1:], nil
|
||||
}
|
||||
|
||||
// return formatted error if quoted string is not terminated
|
||||
valEndIndex := bytes.IndexFunc(src, isCharFunc('\n'))
|
||||
if valEndIndex == -1 {
|
||||
valEndIndex = len(src)
|
||||
}
|
||||
|
||||
return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex])
|
||||
}
|
||||
|
||||
func expandEscapes(str string) string {
|
||||
out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string {
|
||||
c := strings.TrimPrefix(match, `\`)
|
||||
switch c {
|
||||
case "n":
|
||||
return "\n"
|
||||
case "r":
|
||||
return "\r"
|
||||
default:
|
||||
return match
|
||||
}
|
||||
})
|
||||
return unescapeCharsRegex.ReplaceAllString(out, "$1")
|
||||
}
|
||||
|
||||
func indexOfNonSpaceChar(src []byte) int {
|
||||
return bytes.IndexFunc(src, func(r rune) bool {
|
||||
return !unicode.IsSpace(r)
|
||||
})
|
||||
}
|
||||
|
||||
// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character
|
||||
func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) {
|
||||
if len(src) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
switch prefix := src[0]; prefix {
|
||||
case prefixDoubleQuote, prefixSingleQuote:
|
||||
return prefix, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func isCharFunc(char rune) func(rune) bool {
|
||||
return func(v rune) bool {
|
||||
return v == char
|
||||
}
|
||||
}
|
||||
|
||||
// isSpace reports whether the rune is a space character but not line break character
|
||||
//
|
||||
// this differs from unicode.IsSpace, which also applies line break as space
|
||||
func isSpace(r rune) bool {
|
||||
switch r {
|
||||
case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isLineEnd(r rune) bool {
|
||||
if r == '\n' || r == '\r' {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
escapeRegex = regexp.MustCompile(`\\.`)
|
||||
expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)
|
||||
unescapeCharsRegex = regexp.MustCompile(`\\([^$])`)
|
||||
)
|
||||
|
||||
func expandVariables(v string, m map[string]string) string {
|
||||
return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string {
|
||||
submatch := expandVarRegex.FindStringSubmatch(s)
|
||||
|
||||
if submatch == nil {
|
||||
return s
|
||||
}
|
||||
if submatch[1] == "\\" || submatch[2] == "(" {
|
||||
return submatch[0][1:]
|
||||
} else if submatch[4] != "" {
|
||||
return m[submatch[4]]
|
||||
}
|
||||
return s
|
||||
})
|
||||
}
|
||||
Vendored
+6
@@ -127,6 +127,9 @@ github.com/aws/smithy-go/time
|
||||
github.com/aws/smithy-go/tracing
|
||||
github.com/aws/smithy-go/transport/http
|
||||
github.com/aws/smithy-go/transport/http/internal/io
|
||||
# github.com/caarlos0/env/v11 v11.3.1
|
||||
## explicit; go 1.18
|
||||
github.com/caarlos0/env/v11
|
||||
# github.com/cenkalti/backoff/v4 v4.3.0
|
||||
## explicit; go 1.18
|
||||
github.com/cenkalti/backoff/v4
|
||||
@@ -284,6 +287,9 @@ github.com/jackc/pgx/v5/pgxpool
|
||||
## explicit; go 1.19
|
||||
github.com/jackc/puddle/v2
|
||||
github.com/jackc/puddle/v2/internal/genstack
|
||||
# github.com/joho/godotenv v1.5.1
|
||||
## explicit; go 1.12
|
||||
github.com/joho/godotenv
|
||||
# github.com/josharian/intern v1.0.0
|
||||
## explicit; go 1.5
|
||||
github.com/josharian/intern
|
||||
|
||||
Reference in New Issue
Block a user