Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add --strict. #34

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .drone.yml
Expand Up @@ -3,6 +3,6 @@ name: default

steps:
- name: build
image: golang:1.11
image: golang:1.19
commands:
- go test -v ./...
10 changes: 8 additions & 2 deletions cmd/envsubst/main.go
Expand Up @@ -2,19 +2,22 @@ package main

import (
"bufio"
"flag"
"fmt"
"log"
"os"

"github.com/drone/envsubst/v2"
)

var flagStrict bool = false

func main() {
flag.Parse()
stdin := bufio.NewScanner(os.Stdin)
stdout := bufio.NewWriter(os.Stdout)

for stdin.Scan() {
line, err := envsubst.EvalEnv(stdin.Text())
line, err := envsubst.EvalEnv(stdin.Text(), flagStrict)
if err != nil {
log.Fatalf("Error while envsubst: %v", err)
}
Expand All @@ -26,3 +29,6 @@ func main() {
}
}

func init() {
flag.BoolVar(&flagStrict, "strict", false, "fail if variable is undefined.")
}
14 changes: 11 additions & 3 deletions eval.go
Expand Up @@ -3,7 +3,7 @@ package envsubst
import "os"

// Eval replaces ${var} in the string based on the mapping function.
func Eval(s string, mapping func(string) string) (string, error) {
func Eval(s string, mapping func(string) (string, bool)) (string, error) {
t, err := Parse(s)
if err != nil {
return s, err
Expand All @@ -14,6 +14,14 @@ func Eval(s string, mapping func(string) string) (string, error) {
// EvalEnv replaces ${var} in the string according to the values of the
// current environment variables. References to undefined variables are
// replaced by the empty string.
func EvalEnv(s string) (string, error) {
return Eval(s, os.Getenv)
func EvalEnv(s string, strict bool) (string, error) {
mapping := Getenv
if strict{
mapping = os.LookupEnv
}
return Eval(s, mapping)
}

func Getenv(s string) (string, bool) {
return os.Getenv(s), true
}
62 changes: 59 additions & 3 deletions eval_test.go
@@ -1,6 +1,9 @@
package envsubst

import "testing"
import (
"errors"
"testing"
)

// test cases sourced from tldp.org
// http://www.tldp.org/LDP/abs/html/parameter-substitution.html
Expand Down Expand Up @@ -210,8 +213,8 @@ func TestExpand(t *testing.T) {
for _, expr := range expressions {
t.Run(expr.input, func(t *testing.T) {
t.Logf(expr.input)
output, err := Eval(expr.input, func(s string) string {
return expr.params[s]
output, err := Eval(expr.input, func(s string) (string, bool) {
return expr.params[s], true
})
if err != nil {
t.Errorf("Want %q expanded but got error %q", expr.input, err)
Expand All @@ -226,3 +229,56 @@ func TestExpand(t *testing.T) {
})
}
}

func TestExpandStrict(t *testing.T) {
var expressions = []struct {
params map[string]string
input string
output string
wantErr error
}{
// text-only
{
params: map[string]string{},
input: "abcdEFGH28ij",
output: "abcdEFGH28ij",
wantErr: nil,
},
// existing
{
params: map[string]string{"foo": "bar"},
input: "${foo}",
output: "bar",
wantErr: nil,
},
// missing
{
params: map[string]string{},
input: "${missing}",
output: "",
wantErr: errVarNotSet,
},
}

for _, expr := range expressions {
t.Run(expr.input, func(t *testing.T) {
t.Logf(expr.input)
output, err := Eval(expr.input, func(s string) (string, bool) {
v, exists := expr.params[s]
return v, exists
})
if expr.wantErr == nil && err != nil {
t.Errorf("Want %q expanded but got error %q", expr.input, err)
}
if expr.wantErr != nil && !errors.Is(err, expr.wantErr) {
t.Errorf("Want error %q but got error %q", expr.wantErr, err)
}
if output != expr.output {
t.Errorf("Want %q expanded to %q, got %q",
expr.input,
expr.output,
output)
}
})
}
}
4 changes: 4 additions & 0 deletions readme.md
Expand Up @@ -34,6 +34,10 @@ Includes support for bash string replacement functions.

For a deeper reference, see [bash-hackers](https://wiki.bash-hackers.org/syntax/pe#case_modification) or [gnu pattern matching](https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html).

## Strict mode

Use `--strict` if you want `envsubst` to fail, if an undefined variable gets accessed.

## Unsupported Functions

* `${var-default}`
Expand Down
12 changes: 9 additions & 3 deletions template.go
Expand Up @@ -2,6 +2,7 @@ package envsubst

import (
"bytes"
"fmt"
"io"
"io/ioutil"

Expand All @@ -16,7 +17,7 @@ type state struct {
node parse.Node // current node

// maps variable names to values
mapper func(string) string
mapper func(string) (value string, exists bool)
}

// Template is the representation of a parsed shell format string.
Expand Down Expand Up @@ -46,7 +47,7 @@ func ParseFile(path string) (*Template, error) {
}

// Execute applies a parsed template to the specified data mapping.
func (t *Template) Execute(mapping func(string) string) (str string, err error) {
func (t *Template) Execute(mapping func(string) (string, bool)) (str string, err error) {
b := new(bytes.Buffer)
s := new(state)
s.node = t.tree.Root
Expand Down Expand Up @@ -87,6 +88,8 @@ func (t *Template) evalList(s *state, node *parse.ListNode) (err error) {
return nil
}

var errVarNotSet = fmt.Errorf("variable not set (strict mode)")

func (t *Template) evalFunc(s *state, node *parse.FuncNode) error {
var w = s.writer
var buf bytes.Buffer
Expand All @@ -106,8 +109,11 @@ func (t *Template) evalFunc(s *state, node *parse.FuncNode) error {
s.writer = w
s.node = node

v := s.mapper(node.Param)
v, exists := s.mapper(node.Param)

if node.Name == "" && !exists {
return fmt.Errorf("%w: %q", errVarNotSet, node.Param)
}
fn := lookupFunc(node.Name, len(args))

_, err := io.WriteString(s.writer, fn(v, args...))
Expand Down