initial commit

This commit is contained in:
Tordarus 2025-02-01 19:24:56 +01:00
commit 740b8c6df2
8 changed files with 278 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
loadenv
*.env

19
LICENSE.md Normal file
View File

@ -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.

69
README.md Normal file
View File

@ -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
```

69
expand_files.go Normal file
View File

@ -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
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.tordarus.net/tordarus/loadenv
go 1.18

2
go.sum Normal file
View File

@ -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=

100
main.go Normal file
View File

@ -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())
}
}

14
sliceflag.go Normal file
View File

@ -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
}