Add first iteration and test suite
This commit is contained in:
parent
ae05a87e83
commit
c0f362d9bb
|
@ -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 ./...
|
|
@ -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.
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue