package main import ( "bufio" "bytes" "errors" "flag" "fmt" "io" "log/slog" "os" "os/exec" "path/filepath" "regexp" "strings" "text/template" "time" "github.com/acaloiaro/go-envparse" ) var ( version = "dev" commit = "none" date = time.Now().Format(time.RFC3339) ) type exampleFlag map[string]string func (e exampleFlag) String() string { var str string for key, val := range e { str = fmt.Sprintf(`%s --example="%s=%s"`, str, key, val) } return str } func (e exampleFlag) Set(value string) (err error) { key, value, found := strings.Cut(value, "=") if !found { err = errors.New("examples must be provided in KEY=VALUE format") return } e[key] = value return nil } var ( examplesFlag = make(exampleFlag) debugFlag bool skipGitAddFlag bool envFileFlag string sampleFileFlag string versionFlag bool ) func init() { flag.StringVar(&envFileFlag, "env-file", ".env", "set the path to your env file: ess -env-file=.env_file [sync|install]") flag.StringVar(&sampleFileFlag, "sample-file", "env.sample", "set the path to your sample file: ess -sample-file=env_var.sample [sync|install]") flag.BoolVar(&debugFlag, "debug", false, "print debug logs: ess --debug [sync|install]") flag.BoolVar(&skipGitAddFlag, "skip-git-add", false, "skip doing 'git add' on generated sample file after sync") flag.Var(examplesFlag, "example", "set example values for samples: ess --example=BAR=\"my bar value\" [sync|install]") flag.BoolVar(&versionFlag, "version", false, "print the current ess version: ess --version") flag.Usage = func() { cmd := os.Args[0] fmt.Fprintf(os.Stderr, "Usage of %s:\n", cmd) fmt.Fprintf(os.Stderr, "%s [-flags] %s \n\n", cmd, "[install|sync]") flag.PrintDefaults() } flag.Parse() if versionFlag { fmt.Fprintf(os.Stdout, "ess version: %s built at: %s from commit: %s\n", version, date, commit) os.Exit(0) } if debugFlag { slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))) } } func main() { command := "sync" args := flag.Args() if len(args) > 0 { command = args[0] } projectPath, gitDirPath := gitDirPaths() switch command { case "sync": sync(projectPath) if skipGitAddFlag { return } cmd := exec.Command("git", "add", filepath.Join(projectPath, sampleFileFlag)) slog.Debug("running git command", "args", cmd.Args) err := cmd.Run() if err != nil { fmt.Printf("unable to add sample file '%s' to git: %v", sampleFileFlag, err) os.Exit(1) } case "install": slog.Debug("installing hook to", "path", gitDirPath) err := installHook(gitDirPath) if err != nil { fmt.Println("unable to install pre-commit hook:", err) os.Exit(1) } default: fmt.Printf("ess: unknown command '%s'\n\n", command) flag.Usage() os.Exit(1) } } func sync(dir string) { envFilePath := filepath.Join(dir, envFileFlag) sampleFilePath := filepath.Join(dir, sampleFileFlag) slog.Debug("syncing env file with sample", "env_file", envFilePath, "sample_file", sampleFilePath) envFileReader, err := os.Open(envFilePath) if err != nil { fmt.Printf("env file '%s' was not found. skipping sync.\n", envFilePath) os.Exit(0) } envFile, err := envparse.ParsePermissive(envFileReader) if err != nil { fmt.Fprintf(os.Stderr, "unable to parse env file (%s): %v", envFilePath, err) os.Exit(1) } scrubEnvFile(envFile, examplesFlag) err = writeSampleFile(envFile, envFilePath, sampleFilePath) if err != nil { fmt.Println(err) os.Exit(1) } slog.Debug("sample file written", "sample_file", sampleFilePath) } func writeSampleFile(sampleFileContent map[string]string, envFilePath, sampleFilePath string) (err error) { sampleFile, err := os.Create(sampleFilePath) if err != nil { err = fmt.Errorf("unable to create sample file: %w", err) return } defer sampleFile.Close() sampleWriter := bufio.NewWriter(sampleFile) envFile, err := os.Open(envFilePath) if err != nil { err = fmt.Errorf("unable to read env file: %w", err) return } defer envFile.Close() scanner := bufio.NewScanner(envFile) // interate through the env file line-by-line, searching for environment variables // any line that starts with a key from `sampleFileContents` followed by an equal sign `=` is considered // a line that contains a secret for scanner.Scan() { envFileLine := scanner.Text() scrubbedLine := replaceSecrets(envFileLine, sampleFileContent) sampleWriter.WriteString(fmt.Sprintf("%s\n", scrubbedLine)) } sampleWriter.Flush() return } func scrubEnvFile(envFile map[string]string, examples map[string]string) { slog.Debug("scrubbing env file of secrets") for envFileKey := range envFile { exampleVal, ok := examples[envFileKey] if ok { envFile[envFileKey] = exampleVal } else { envFile[envFileKey] = fmt.Sprintf("<%s>", envFileKey) } } } // replaceSecrets replaces the content of env file entries (lines) with a new line that is scrubbed of secrets // // any line containing a variable name followed by and equal sign is considered to contain a secret func replaceSecrets(envFileEntry string, sampleFileContent map[string]string) (newLine string) { var r *regexp.Regexp newLine = envFileEntry for secretKey, secretPlaceholder := range sampleFileContent { r = regexp.MustCompile(fmt.Sprintf("(%s.*=.*)", secretKey)) if r.MatchString(envFileEntry) { slog.Debug("replacing secrets with sample values", "secret_key", secretKey, "placeholder", secretPlaceholder) newLine = r.ReplaceAllString(envFileEntry, fmt.Sprintf("%s=%s", secretKey, secretPlaceholder)) } } return } // gitDirPath returns the path the the GIT_DIR func gitDirPaths() (projectPath, gitDir string) { var outb, errb bytes.Buffer cmd := exec.Command("git", "rev-parse", "--show-toplevel") cmd.Stdout = &outb cmd.Stderr = &errb err := cmd.Run() if err != nil { fmt.Println("unable to find project directory:", err) os.Exit(1) } projectPath = strings.TrimRight(outb.String(), "\r\n") outb.Reset() errb.Reset() cmd = exec.Command("git", "rev-parse", "--git-dir") cmd.Stdout = &outb cmd.Stderr = &errb err = cmd.Run() if err != nil { fmt.Println("unable to find GIT_DIR:", err) os.Exit(1) } gitDir = strings.TrimRight(outb.String(), "\r\n") gitDir = filepath.Join(projectPath, gitDir) return } var preCommitScriptTemplate = `{{if .Appending}}{{ .OldPreCommitScript }} # Below code generated by ess https://github.com/acaloiaro/ess {{else}}#!/usr/bin/env bash # File generated by ess https://github.com/acaloiaro/ess {{end}}ARGS=({{ .Args }}) exec $(dirname -- "${BASH_SOURCE[0]}")/{{ .PreCommitHooksDir }}/0-ess "${ARGS[@]}" ` // installHook installs the ess git hook to 'dir' func installHook(gitDirPath string) (err error) { hooksScriptDirName := "pre-commit-hooks.d" hooksDirName := filepath.Join(gitDirPath, "hooks") hooksScriptDirPath := filepath.Join(hooksDirName, hooksScriptDirName) preCommitHookScriptPath := filepath.Join(hooksDirName, "pre-commit") appendFlag := false var oldPreCommitScript []byte _, err = os.Stat(preCommitHookScriptPath) if !os.IsNotExist(err) { fmt.Printf("A pre-commit hook already exists. Would you like to cancel [c], overwrite [o], or append [a] the existing pre-commit hook script? [a, c, o]: ") var response string fmt.Scanln(&response) if response == "c" || (response != "o" && response != "a") { slog.Debug("user declined to overwrite the existing pre-commit hook") os.Exit(0) } if response == "a" { slog.Debug("will append pre-commit hook script to existing script", "script_path", preCommitHookScriptPath) // read the existing content of the pre-commit file oldPreCommitScript, err = os.ReadFile(preCommitHookScriptPath) if err != nil { return err } appendFlag = true } } // Create a directory to place pre-commit-hook executable within if _, err = os.Stat(hooksScriptDirPath); os.IsNotExist(err) { if err = os.MkdirAll(hooksScriptDirPath, os.ModePerm); err != nil { return } } slog.Debug("hook will be installed in", "dir_path", hooksScriptDirPath) preCommitHookPath := filepath.Join(hooksScriptDirPath, "0-ess") preCommitHook, err := os.OpenFile(preCommitHookPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755) if err != nil { return } executablePath, err := exec.LookPath(os.Args[0]) if err != nil { fmt.Println("unable to find 'ess' in $PATH:", err) os.Exit(1) } executable, err := os.Open(executablePath) if err != nil { return } // copy the currently running executable wholsale into the pre commit hooks directory io.Copy(preCommitHook, executable) tmpl, err := template.New("pre-commit-script").Parse(preCommitScriptTemplate) if err != nil { return err } args := fmt.Sprintf("--env-file=%s --sample-file=%s %s", envFileFlag, sampleFileFlag, examplesFlag) buff := bytes.NewBufferString("") templateValues := struct { Args string PreCommitHooksDir string Appending bool OldPreCommitScript string }{ Args: args, PreCommitHooksDir: hooksScriptDirName, Appending: appendFlag, OldPreCommitScript: string(oldPreCommitScript), } slog.Debug("hook metadata", "data", templateValues) err = tmpl.Execute(buff, templateValues) if err != nil { return err } preCommitHookScript, err := os.OpenFile(preCommitHookScriptPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755) if err != nil { return err } defer preCommitHookScript.Close() _, err = buff.WriteTo(preCommitHookScript) if err != nil { return err } fmt.Println("ess pre-commit hook installed!") return nil }