366 lines
9.3 KiB
Go
366 lines
9.3 KiB
Go
package ezconf
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// PrintEnvVars prints all environment variables to the provided writer
|
|
func (cl *ConfigLoader) PrintEnvVars(w io.Writer, showValues bool) error {
|
|
if len(cl.envVars) == 0 {
|
|
return errors.New("no environment variables loaded (did you call Load()?)")
|
|
}
|
|
|
|
// Group variables by their Group field
|
|
groups := make(map[string][]EnvVar)
|
|
groupOrder := make([]string, 0)
|
|
|
|
for _, envVar := range cl.envVars {
|
|
group := envVar.Group
|
|
if group == "" {
|
|
group = "Other"
|
|
}
|
|
|
|
if _, exists := groups[group]; !exists {
|
|
groupOrder = append(groupOrder, group)
|
|
}
|
|
groups[group] = append(groups[group], envVar)
|
|
}
|
|
|
|
// Print variables grouped by section
|
|
for _, group := range groupOrder {
|
|
vars := groups[group]
|
|
|
|
// Calculate max name length for alignment within this group
|
|
maxNameLen := 0
|
|
for _, envVar := range vars {
|
|
nameLen := len(envVar.Name)
|
|
if showValues {
|
|
value := envVar.CurrentValue
|
|
if value == "" && envVar.Default != "" {
|
|
value = envVar.Default
|
|
}
|
|
nameLen += len(value) + 1 // +1 for the '=' sign
|
|
}
|
|
if nameLen > maxNameLen {
|
|
maxNameLen = nameLen
|
|
}
|
|
}
|
|
|
|
// Print group header
|
|
fmt.Fprintf(w, "\n%s Configuration\n", group)
|
|
fmt.Fprintln(w, strings.Repeat("=", len(group)+14))
|
|
fmt.Fprintln(w)
|
|
|
|
for _, envVar := range vars {
|
|
// Build the variable line
|
|
var varLine string
|
|
if showValues {
|
|
value := envVar.CurrentValue
|
|
if value == "" && envVar.Default != "" {
|
|
value = envVar.Default
|
|
}
|
|
varLine = fmt.Sprintf("%s=%s", envVar.Name, value)
|
|
} else {
|
|
varLine = envVar.Name
|
|
}
|
|
|
|
// Calculate padding for alignment
|
|
padding := maxNameLen - len(varLine) + 2
|
|
|
|
// Print with indentation and alignment
|
|
fmt.Fprintf(w, " %s%s# %s", varLine, strings.Repeat(" ", padding), envVar.Description)
|
|
|
|
if envVar.Required {
|
|
fmt.Fprint(w, " (required)")
|
|
}
|
|
if envVar.Default != "" {
|
|
fmt.Fprintf(w, " (default: %s)", envVar.Default)
|
|
}
|
|
fmt.Fprintln(w)
|
|
}
|
|
}
|
|
|
|
fmt.Fprintln(w)
|
|
|
|
return nil
|
|
}
|
|
|
|
// PrintEnvVarsStdout prints all environment variables to stdout
|
|
func (cl *ConfigLoader) PrintEnvVarsStdout(showValues bool) error {
|
|
return cl.PrintEnvVars(os.Stdout, showValues)
|
|
}
|
|
|
|
// GenerateEnvFile creates a new .env file with all environment variables
|
|
// If the file already exists, it will preserve any untracked variables
|
|
func (cl *ConfigLoader) GenerateEnvFile(filename string, useCurrentValues bool) error {
|
|
// Check if file exists and parse it to preserve untracked variables
|
|
var existingUntracked []envFileLine
|
|
if _, err := os.Stat(filename); err == nil {
|
|
existingVars, err := parseEnvFile(filename)
|
|
if err == nil {
|
|
// Track which variables are managed by ezconf
|
|
managedVars := make(map[string]bool)
|
|
for _, envVar := range cl.envVars {
|
|
managedVars[envVar.Name] = true
|
|
}
|
|
|
|
// Collect untracked variables
|
|
for _, line := range existingVars {
|
|
if line.IsVar && !managedVars[line.Key] {
|
|
existingUntracked = append(existingUntracked, line)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
file, err := os.Create(filename)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to create env file")
|
|
}
|
|
defer file.Close()
|
|
|
|
writer := bufio.NewWriter(file)
|
|
defer writer.Flush()
|
|
|
|
// Write header
|
|
fmt.Fprintln(writer, "# Environment Configuration")
|
|
fmt.Fprintln(writer, "# Generated by ezconf")
|
|
fmt.Fprintln(writer, "#")
|
|
fmt.Fprintln(writer, "# Variables marked as (required) must be set")
|
|
fmt.Fprintln(writer, "# Variables with defaults can be left commented out to use the default value")
|
|
|
|
// Group variables by their Group field
|
|
groups := make(map[string][]EnvVar)
|
|
groupOrder := make([]string, 0)
|
|
|
|
for _, envVar := range cl.envVars {
|
|
group := envVar.Group
|
|
if group == "" {
|
|
group = "Other"
|
|
}
|
|
|
|
if _, exists := groups[group]; !exists {
|
|
groupOrder = append(groupOrder, group)
|
|
}
|
|
groups[group] = append(groups[group], envVar)
|
|
}
|
|
|
|
// Write variables grouped by section
|
|
for _, group := range groupOrder {
|
|
vars := groups[group]
|
|
|
|
// Print group header
|
|
fmt.Fprintln(writer)
|
|
fmt.Fprintf(writer, "# %s Configuration\n", group)
|
|
fmt.Fprintln(writer, strings.Repeat("#", len(group)+15))
|
|
|
|
for _, envVar := range vars {
|
|
// Write comment with description
|
|
fmt.Fprintf(writer, "# %s", envVar.Description)
|
|
if envVar.Required {
|
|
fmt.Fprint(writer, " (required)")
|
|
}
|
|
if envVar.Default != "" {
|
|
fmt.Fprintf(writer, " (default: %s)", envVar.Default)
|
|
}
|
|
fmt.Fprintln(writer)
|
|
|
|
// Get value to write
|
|
value := ""
|
|
if useCurrentValues && envVar.CurrentValue != "" {
|
|
value = envVar.CurrentValue
|
|
} else if envVar.Default != "" {
|
|
value = envVar.Default
|
|
}
|
|
|
|
// Comment out optional variables with defaults
|
|
if !envVar.Required && envVar.Default != "" && (!useCurrentValues || envVar.CurrentValue == "") {
|
|
fmt.Fprintf(writer, "# %s=%s\n", envVar.Name, value)
|
|
} else {
|
|
fmt.Fprintf(writer, "%s=%s\n", envVar.Name, value)
|
|
}
|
|
|
|
fmt.Fprintln(writer)
|
|
}
|
|
}
|
|
|
|
// Write untracked variables from existing file
|
|
if len(existingUntracked) > 0 {
|
|
fmt.Fprintln(writer)
|
|
fmt.Fprintln(writer, "# Untracked Variables")
|
|
fmt.Fprintln(writer, "# These variables were in the original file but are not managed by ezconf")
|
|
fmt.Fprintln(writer, strings.Repeat("#", 72))
|
|
fmt.Fprintln(writer)
|
|
|
|
for _, line := range existingUntracked {
|
|
fmt.Fprintf(writer, "%s=%s\n", line.Key, line.Value)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateEnvFile updates an existing .env file with new variables or updates existing ones
|
|
func (cl *ConfigLoader) UpdateEnvFile(filename string, createIfNotExist bool) error {
|
|
// Check if file exists
|
|
_, err := os.Stat(filename)
|
|
if os.IsNotExist(err) {
|
|
if createIfNotExist {
|
|
return cl.GenerateEnvFile(filename, false)
|
|
}
|
|
return errors.Errorf("env file does not exist: %s", filename)
|
|
}
|
|
|
|
// Read existing file
|
|
existingVars, err := parseEnvFile(filename)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to parse existing env file")
|
|
}
|
|
|
|
// Create a map for quick lookup
|
|
existingMap := make(map[string]string)
|
|
for _, line := range existingVars {
|
|
if line.IsVar {
|
|
existingMap[line.Key] = line.Value
|
|
}
|
|
}
|
|
|
|
// Create new file with updates
|
|
tempFile := filename + ".tmp"
|
|
file, err := os.Create(tempFile)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to create temp file")
|
|
}
|
|
defer file.Close()
|
|
|
|
writer := bufio.NewWriter(file)
|
|
defer writer.Flush()
|
|
|
|
// Track which variables we've written
|
|
writtenVars := make(map[string]bool)
|
|
|
|
// Copy existing file, updating values as needed
|
|
for _, line := range existingVars {
|
|
if line.IsVar {
|
|
// Check if we have this variable in our config
|
|
found := false
|
|
for _, envVar := range cl.envVars {
|
|
if envVar.Name == line.Key {
|
|
found = true
|
|
// Keep existing value if it's set
|
|
if line.Value != "" {
|
|
fmt.Fprintf(writer, "%s=%s\n", line.Key, line.Value)
|
|
} else {
|
|
// Use default if available
|
|
value := envVar.Default
|
|
fmt.Fprintf(writer, "%s=%s\n", line.Key, value)
|
|
}
|
|
writtenVars[envVar.Name] = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
// Variable not in our config, keep it anyway
|
|
fmt.Fprintf(writer, "%s=%s\n", line.Key, line.Value)
|
|
}
|
|
} else {
|
|
// Comment or empty line, keep as-is
|
|
fmt.Fprintln(writer, line.Line)
|
|
}
|
|
}
|
|
|
|
// Add new variables that weren't in the file
|
|
addedNew := false
|
|
for _, envVar := range cl.envVars {
|
|
if !writtenVars[envVar.Name] {
|
|
if !addedNew {
|
|
fmt.Fprintln(writer)
|
|
fmt.Fprintln(writer, "# New variables added by ezconf")
|
|
addedNew = true
|
|
}
|
|
|
|
// Write comment with description
|
|
fmt.Fprintf(writer, "# %s", envVar.Description)
|
|
if envVar.Required {
|
|
fmt.Fprint(writer, " (required)")
|
|
}
|
|
if envVar.Default != "" {
|
|
fmt.Fprintf(writer, " (default: %s)", envVar.Default)
|
|
}
|
|
fmt.Fprintln(writer)
|
|
|
|
// Write variable with default value
|
|
value := envVar.Default
|
|
fmt.Fprintf(writer, "%s=%s\n", envVar.Name, value)
|
|
fmt.Fprintln(writer)
|
|
}
|
|
}
|
|
|
|
writer.Flush()
|
|
file.Close()
|
|
|
|
// Replace original file with updated one
|
|
if err := os.Rename(tempFile, filename); err != nil {
|
|
return errors.Wrap(err, "failed to replace env file")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// envFileLine represents a line in an .env file
|
|
type envFileLine struct {
|
|
Line string // The full line
|
|
IsVar bool // Whether this is a variable assignment
|
|
Key string // Variable name (if IsVar is true)
|
|
Value string // Variable value (if IsVar is true)
|
|
}
|
|
|
|
// parseEnvFile parses an .env file and returns all lines
|
|
func parseEnvFile(filename string) ([]envFileLine, error) {
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to open file")
|
|
}
|
|
defer file.Close()
|
|
|
|
lines := make([]envFileLine, 0)
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
trimmed := strings.TrimSpace(line)
|
|
|
|
// Check if this is a variable assignment
|
|
if trimmed != "" && !strings.HasPrefix(trimmed, "#") && strings.Contains(trimmed, "=") {
|
|
parts := strings.SplitN(trimmed, "=", 2)
|
|
if len(parts) == 2 {
|
|
lines = append(lines, envFileLine{
|
|
Line: line,
|
|
IsVar: true,
|
|
Key: strings.TrimSpace(parts[0]),
|
|
Value: strings.TrimSpace(parts[1]),
|
|
})
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Comment or empty line
|
|
lines = append(lines, envFileLine{
|
|
Line: line,
|
|
IsVar: false,
|
|
})
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, errors.Wrap(err, "failed to scan file")
|
|
}
|
|
|
|
return lines, nil
|
|
}
|