Add first iteration and test suite
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build was killed Details

This commit is contained in:
Rob Watson 2022-05-16 02:06:05 +02:00
parent ae05a87e83
commit c0f362d9bb
7 changed files with 391 additions and 0 deletions

14
.drone.yml Normal file
View File

@ -0,0 +1,14 @@
---
kind: pipeline
type: docker
name: default
steps:
- name: go-1.18
image: golang:1.18
commands:
- go install honnef.co/go/tools/cmd/staticcheck@latest
- go build ./...
- go vet ./...
- staticcheck ./...
- go test -bench=. -benchmem -cover ./...

61
README.md Normal file
View File

@ -0,0 +1,61 @@
# envfilesubst
envfilesubst is a variation of gettext's envsubst, with a different modus
operandi.
Firstly, instead of reading the current environment, it instead parses a file
in "envfile" format.
Secondly, it will read from standard input and replace all variable references
that can be found in the envfile. If variables are not explicitly mentioned in
the envfile, the references will be left intact (instead of replacing them with
an empty string).
## Installation
```
go install git.netflux.io/rob/envfilesubst@latest
```
## Usage
Given an envfile:
```
FOO=bar
X=1
```
Then:
```
echo "FOO is $FOO and X is ${X}. I don't know $BAR." | envfilesubst -f myenvfile
```
The output is:
```
FOO is bar and X is 1. I don't know $BAR.
```
## License
Copyright © 2022 Rob Watson.
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.

14
go.mod Normal file
View File

@ -0,0 +1,14 @@
module git.netflux.io/rob/envfilesubst
go 1.18
require (
github.com/hashicorp/go-envparse v0.0.0-20200406174449-d9cfd743a15e
github.com/stretchr/testify v1.7.1
)
require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)

13
go.sum Normal file
View File

@ -0,0 +1,13 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/hashicorp/go-envparse v0.0.0-20200406174449-d9cfd743a15e h1:v1d9+AJMP6i4p8BSKNU0InuvmIAdZjQLNN19V86AG4Q=
github.com/hashicorp/go-envparse v0.0.0-20200406174449-d9cfd743a15e/go.mod h1:/NlxCzN2D4C4L2uDE6ux/h6jM+n98VFQM14nnCIfHJU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

47
main.go Normal file
View File

@ -0,0 +1,47 @@
package main
import (
"flag"
"fmt"
"os"
"git.netflux.io/rob/envfilesubst/scanner"
)
const version = "0.1"
func main() {
var (
path string
printVersion bool
)
flag.StringVar(&path, "f", "", "The envfile to read from")
flag.BoolVar(&printVersion, "version", false, "Print version and exit")
flag.Parse()
if printVersion {
fmt.Fprintf(os.Stderr, "envfilesubst version %s\n", version)
os.Exit(0)
}
if path == "" {
fmt.Fprint(os.Stderr, "envfilesubst reads an input from stdin and pipes it to stdout, replacing $ENV_VAR or ${ENV_VAR} occurrences that can be found in the provided envfile.\n\n")
fmt.Fprint(os.Stderr, "Variables that cannot be found in envfile are left unchanged.\n\n")
fmt.Fprint(os.Stderr, "Usage:\n")
flag.PrintDefaults()
os.Exit(1)
}
envfile, err := os.Open(path)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v", err)
os.Exit(1)
}
scanner := scanner.New(os.Stdout, os.Stdin, envfile)
if err := scanner.Scan(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v", err)
os.Exit(1)
}
}

76
scanner/scanner.go Normal file
View File

@ -0,0 +1,76 @@
package scanner
import (
"bufio"
"fmt"
"io"
"regexp"
"strings"
"github.com/hashicorp/go-envparse"
)
var envvarRegex = regexp.MustCompile(`\$\{?([A-Z][A-Z0-9_]*)\}?`)
type Scanner struct {
envfile, r io.Reader
w io.Writer
}
// New returns a new Scanner with the provided arguments.
func New(w io.Writer, r io.Reader, envfile io.Reader) *Scanner {
return &Scanner{
w: w,
r: r,
envfile: envfile,
}
}
const nl = "\n"
func (s *Scanner) Scan() error {
vars, err := envparse.Parse(s.envfile)
if err != nil {
return fmt.Errorf("error parsing envfile: %v", err)
}
scanner := bufio.NewScanner(s.r)
for scanner.Scan() {
text := scanner.Text()
matchIndices := envvarRegex.FindAllStringSubmatchIndex(text, -1)
var sb strings.Builder
var c int
for _, idx := range matchIndices {
m1, m2, n1, n2 := idx[0], idx[1], idx[2], idx[3]
writeString(&sb, text[c:m1])
c = m2
name := text[n1:n2]
if val, ok := vars[name]; ok {
writeString(&sb, val)
} else {
writeString(&sb, text[m1:m2])
}
}
writeString(&sb, text[c:])
writeString(&sb, nl)
if _, err := s.w.Write([]byte(sb.String())); err != nil {
return fmt.Errorf("error writing to output: %v", err)
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("error reading input: %v", err)
}
return nil
}
// writeString writes a string to a StringBuilder, discarding the result and
// (non-existent) error.
func writeString(sb *strings.Builder, s string) {
_, _ = sb.WriteString(s)
}

166
scanner/scanner_test.go Normal file
View File

@ -0,0 +1,166 @@
package scanner_test
import (
"bytes"
"strings"
"testing"
"git.netflux.io/rob/envfilesubst/scanner"
"github.com/stretchr/testify/assert"
)
func TestScanner(t *testing.T) {
envfile := `FOO=bar
BAR=baz
BAZ=123
FOO_BAR=true
QUUX1=2
`
testCases := []struct {
name string
envfile string
input string
wantOutput string
wantError string
}{
{
name: "single variable",
envfile: envfile,
input: "$FOO",
wantOutput: "bar\n",
},
{
name: "input with prefix text",
envfile: envfile,
input: "baz $FOO",
wantOutput: "baz bar\n",
},
{
name: "input with suffix text",
envfile: envfile,
input: "$FOO baz",
wantOutput: "bar baz\n",
},
{
name: "input with prefix and suffix text",
envfile: envfile,
input: "baz $FOO qux",
wantOutput: "baz bar qux\n",
},
{
name: "input with prefix and suffix text and whitespace",
envfile: envfile,
input: "\tbaz $FOO qux ",
wantOutput: "\tbaz bar qux \n",
},
{
name: "single variable with curly brackets",
envfile: envfile,
input: "${FOO}",
wantOutput: "bar\n",
},
{
name: "multiple variables on a single line",
envfile: envfile,
input: "qux ${FOO} quxx $BAR $BAZ",
wantOutput: "qux bar quxx baz 123\n",
},
{
name: "non-existent variable",
envfile: envfile,
input: "$NOPE",
wantOutput: "$NOPE\n",
},
{
name: "non-existent variable with curly brackets",
envfile: envfile,
input: "${NOPE}",
wantOutput: "${NOPE}\n",
},
{
name: "multiple variables including non-existent",
envfile: envfile,
input: "$FOO $BAR $NOPE $BAZ",
wantOutput: "bar baz $NOPE 123\n",
},
{
name: "variable name with an underscore",
envfile: envfile,
input: "$FOO_BAR is true",
wantOutput: "true is true\n",
},
{
name: "variable name with a number",
envfile: envfile,
input: "$QUUX1 + ${QUUX1} = 4",
wantOutput: "2 + 2 = 4\n",
},
{
name: "multiline input ending with newline",
envfile: envfile,
input: `---
metadata:
name: "$FOO"
labels:
bar: "$BAR"
baz: "$BAZ"
`,
wantOutput: `---
metadata:
name: "bar"
labels:
bar: "baz"
baz: "123"
`,
},
{
name: "multiline input not ending with newline",
envfile: envfile,
input: `---
metadata:
name: "$FOO"
labels:
bar: "$BAR"
baz: "$BAZ"`,
wantOutput: `---
metadata:
name: "bar"
labels:
bar: "baz"
baz: "123"
`,
},
{
name: "empty string",
envfile: envfile,
input: "",
wantOutput: "",
},
{
name: "multiline with only newlines",
envfile: envfile,
input: "\n\n\n\n\n",
wantOutput: "\n\n\n\n\n",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
envfile := strings.NewReader(strings.TrimSpace(tc.envfile))
input := strings.NewReader(tc.input)
var output bytes.Buffer
scanner := scanner.New(&output, input, envfile)
err := scanner.Scan()
if tc.wantError == "" {
assert.NoError(t, err)
assert.Equal(t, tc.wantOutput, output.String())
} else {
assert.EqualError(t, err, tc.wantError)
}
})
}
}