Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@

# Dependency directories (remove the comment below to include it)
# vendor/

firstupdotenv
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,33 @@ The output may then look like this:
unset FOO
unset BAR
export FIRSTUPDOTENV_CURRENT_SET_ENV=FOO,BAR
export FOO=value1
export BAR=value2
export FOO="value1"
export BAR="value2"
```

The `.env` format is a file on the form `key=value`. It ignores empty lines and lines starting with # and lines without an equals sign. If the same key is defined more than once, the last will win.

## 1Password Integration

You can load environment variables from 1Password by adding a line starting with `op://`:

```bash
# Regular variables
FOO=bar

# Load from 1Password
op://Dev/myproject/env
```

The referenced 1Password field should contain line-separated `KEY=value` entries:

```
AWS_ACCESS_KEY=xxx
AWS_SECRET_KEY=yyy
```

This executes a single `op read` command, which is useful since `op` can be slow (~1 second per invocation). All keys loaded from 1Password are tracked and will be unset when you navigate away.

To install:

```bash
Expand Down
47 changes: 44 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package main

import (
"bufio"
"bytes"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
)
Expand Down Expand Up @@ -69,7 +71,7 @@ func createEnvSourceFromCurrentDir() (string, error) {
envSetScript.WriteString(fmt.Sprintf("export FIRSTUPDOTENV_FILE=%s\n", filepath.Join(directory, nameDotEnv)))
} else {
envSetScript.WriteString(fmt.Sprintf("unset %s\n", currentSetEnvVar))
envSetScript.WriteString(fmt.Sprintf("unset FIRSTUPDOTENV_FILE\n"))
envSetScript.WriteString(fmt.Sprintln("unset FIRSTUPDOTENV_FILE"))
}

return envSetScript.String(), nil
Expand Down Expand Up @@ -99,14 +101,15 @@ func loadEnvFile(directory string) (string, error) {
envSetScript.WriteString(fmt.Sprintf("export %s=%s\n", currentSetEnvVar, strings.Join(keys, ",")))

for k, v := range envm {
envSetScript.WriteString(fmt.Sprintf("export %s=%s\n", k, v))
envSetScript.WriteString(fmt.Sprintf("export %s=%q\n", k, v))
}

return envSetScript.String(), nil
}

// parseEnvFile loads environment variables from text file on the form key=value.
// It ignores empty lines and lines starting with # and lines without an equals sign.
// It ignores empty lines and lines starting with #.
// Lines starting with op:// are treated as 1Password references and loaded via `op read`.
func parseEnvFile(filename string) (map[string]string, error) {
fi, err := os.Stat(filename)
if err != nil || fi.IsDir() {
Expand All @@ -121,6 +124,44 @@ func parseEnvFile(filename string) (map[string]string, error) {

env := make(map[string]string)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "op://") {
opEnv, err := readFromOnePassword(line)
if err != nil {
return nil, err
}
for k, v := range opEnv {
env[k] = v
}
continue
}
key, value, found := strings.Cut(line, "=")
if !found {
continue
}
env[strings.TrimSpace(key)] = strings.TrimSpace(value)
}
return env, scanner.Err()
}

// readFromOnePassword reads environment variables from a 1Password reference.
// The reference should be in the form op://vault/item/field.
// The field should contain line-separated KEY=value entries.
func readFromOnePassword(reference string) (map[string]string, error) {
cmd := exec.Command("op", "read", reference)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("op read %s: %w: %s", reference, err, stderr.String())
}

env := make(map[string]string)
scanner := bufio.NewScanner(&stdout)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
Expand Down
4 changes: 2 additions & 2 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ func TestFindEnvInCurrentDir(t *testing.T) {
}

check("export FIRSTUPDOTENV_CURRENT_SET_ENV=FOO,BAR")
check("export FOO=value1")
check("export BAR=value2")
check(`export FOO="value1"`)
check(`export BAR="value2"`)
}
Loading