From 740b8c6df28a0bd9d24b7d672e2ddfc6accbbbc6 Mon Sep 17 00:00:00 2001 From: Tordarus Date: Sat, 1 Feb 2025 19:24:56 +0100 Subject: [PATCH] initial commit --- .gitignore | 2 + LICENSE.md | 19 +++++++++ README.md | 69 +++++++++++++++++++++++++++++++++ expand_files.go | 69 +++++++++++++++++++++++++++++++++ go.mod | 3 ++ go.sum | 2 + main.go | 100 ++++++++++++++++++++++++++++++++++++++++++++++++ sliceflag.go | 14 +++++++ 8 files changed, 278 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 expand_files.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 sliceflag.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb48c42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +loadenv +*.env diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..2a5c4d4 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) 2022 Mila Ringwald + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e992e2 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# loadenv + +Loads environment files before running a command without polluting your environment + +## Source code + +You can find the source code here: [git.milar.in](https://git.milar.in/milarin/loadenv) + +## Installation + +If you have Go installed, you can simply go install the program: `go install git.milar.in/milarin/loadenv@latest` + +There are pre-compiled executables for various platforms on the [repository](https://git.milar.in/milarin/loadenv/releases). + +## License + +Distributed under the MIT License. See [LICENSE.md](https://git.milar.in/milarin/loadenv/src/branch/main/LICENSE.md) + +## Usage + +`loadenv` loads the environment file provided via `-f` into the environment and runs the command provided after the arguments. +If no file is provided, `.env` is used by default. +If no command is provided, `loadenv` prints all environment variables to stdout. + +--- + +The variables will only be available to the given command. They will be deleted after the command exits. +`loadenv` can be useful when working with programs which heavily use environment variables. + +```sh +$ cat .env +# production server configuration +DATABASE_HOST=prod-server.com +DATABASE_USER=milarin +DATABASE_PASS=my-super-secure-password + +$ # show the 3 last added env variables +$ loadenv | tail -n 3 +DATABASE_HOST=prod-server.com +DATABASE_USER=milarin +DATABASE_PASS=my-super-secure-password + +$ cat dev.env +# development server configuration +DATABASE_HOST=dev-server.com +DATABASE_USER=milarin +DATABASE_PASS=my-super-secure-password + +$ # load dev.env into environment and run the command env. show last 3 lines +$ loadenv -f dev.env env | tail -n 3 +DATABASE_HOST=dev-server.com +DATABASE_USER=milarin +DATABASE_PASS=my-super-secure-password +``` + +environment variables in that file will be expanded: + +```sh +$ env | grep USER # current value of $USER +USER=milarin + +$ cat .env +# expand $USER reference +USERNAME=$USER + +$ loadenv | tail -n 1 +USERNAME=milarin +``` + diff --git a/expand_files.go b/expand_files.go new file mode 100644 index 0000000..f526b1f --- /dev/null +++ b/expand_files.go @@ -0,0 +1,69 @@ +package main + +import ( + "bytes" + "io" + "os" +) + +func expandEnvFiles() error { + for _, path := range FlagExpandEnvFiles { + tmpFile, err := os.CreateTemp("", "*") + if err != nil { + return err + } + defer tmpFile.Close() + + ExpandEnvFiles[path] = tmpFile.Name() + + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + data, err := io.ReadAll(file) + if err != nil { + return err + } + file.Close() + + if _, err := io.Copy(tmpFile, bytes.NewReader(data)); err != nil { + return err + } + + newFile, err := os.Create(path) + if err != nil { + return err + } + defer newFile.Close() + + if _, err := newFile.WriteString(os.ExpandEnv(string(data))); err != nil { + return err + } + } + + return nil +} + +func restoreEnvFiles() error { + for originalFilePath, tmpFilePath := range ExpandEnvFiles { + originalFile, err := os.Create(originalFilePath) + if err != nil { + return err + } + defer originalFile.Close() + + tmpFile, err := os.Open(tmpFilePath) + if err != nil { + return err + } + defer tmpFile.Close() + + if _, err := io.Copy(originalFile, tmpFile); err != nil { + return err + } + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..44d5bcd --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.tordarus.net/tordarus/loadenv + +go 1.18 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..780b53f --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +git.milar.in/milarin/buildinfo v1.0.0 h1:tw98GupUYl/0a/3aPGuezhE4wseycOSsbcLp70hy60U= +git.milar.in/milarin/buildinfo v1.0.0/go.mod h1:arI9ZoENOgcZcanv25k9y4dKDUhPp0buJrlVerGruas= diff --git a/main.go b/main.go new file mode 100644 index 0000000..4a80da5 --- /dev/null +++ b/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "bufio" + "flag" + "fmt" + "io" + "os" + "os/exec" + "os/signal" + "regexp" + "syscall" +) + +var ( + EnvVarRegex = regexp.MustCompile(`^(.*?)=(.*?)$`) + EnvCommentRegex = regexp.MustCompile(`^[ \t]*#.*?$`) + ExpandEnvFiles = map[string]string{} +) + +var ( // flags + FlagEnvFilePath = flag.String("f", ".env", "environment file") + + FlagExpandEnvFiles = SliceFlag{} +) + +func main() { + flag.Var(&FlagExpandEnvFiles, "e", "files in which to expand environment variables") + flag.Parse() + + envFile, err := os.Open(*FlagEnvFilePath) + if err != nil { + fmt.Fprintf(os.Stderr, "could not open file '%s': %s\n", *FlagEnvFilePath, err.(*os.PathError).Err) + return + } + defer envFile.Close() + + parseEnvFile(envFile) + args := flag.Args() + + if len(args) == 0 { + for _, envVar := range os.Environ() { + fmt.Println(envVar) + } + return + } + + if err := expandEnvFiles(); err != nil { + fmt.Fprintf(os.Stderr, "could not expand files: %s\n", err.Error()) + return + } + defer restoreFiles() + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + go func() { <-sigs; exit(1) }() + + cmd := exec.Command(args[0], args[1:]...) + cmd.Env = os.Environ() + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + exit(exitErr.ExitCode()) + return + } else { + panic(err) + } + } +} + +func parseEnvFile(r io.Reader) { + s := bufio.NewScanner(r) + l := 0 + + for s.Scan() { + l++ + + matches := EnvVarRegex.FindStringSubmatch(s.Text()) + if len(matches) != 0 { + os.Setenv(matches[1], matches[2]) + } else if s.Text() != "" && !EnvCommentRegex.MatchString(s.Text()) { + panic(fmt.Sprintf("invalid env syntax on line %d", l)) + } + } +} + +func exit(errorCode int) { + restoreFiles() + os.Exit(errorCode) +} + +func restoreFiles() { + if err := restoreEnvFiles(); err != nil { + fmt.Fprintf(os.Stderr, "could not restore expanded files: %s\n", err.Error()) + } +} diff --git a/sliceflag.go b/sliceflag.go new file mode 100644 index 0000000..27df774 --- /dev/null +++ b/sliceflag.go @@ -0,0 +1,14 @@ +package main + +import "strings" + +type SliceFlag []string + +func (f *SliceFlag) String() string { + return strings.Join(*f, ",") +} + +func (f *SliceFlag) Set(value string) error { + *f = append(*f, value) + return nil +}