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 cl.envVars == nil || 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 }