Repository: lyraproj/hiera (go)

Issue:
Provide a single fact on the commandline
In addition to the `--facts` flag (which needs an input file in yaml or json), it'd be cool if the `lookup` command had a way to pass fact name/value on the command line. This would make it easy and fast to test hierarchy or data changes. I'd suggest a syntax like

`lookup --fact name1=value1 --fact name2=value2`

Pull request description:
Improve passing of variables using CLI options
This commit provides the following improvements of how variables can be
set in the Hiera scope by using command line options:

--vars, a repeatable option that appoints yaml or json files on disk, or
  if the option argument is a single '-' (dash) then the yaml or json is
  parsed from stdin.

--var, a repeatable option to provide variables directly on the command
  line using key=value or key:value syntax. The value is expressed using
  Puppet DSL.

--facts, an alias for --vars, is retained for compatibility with the
  Puppet ruby version of Hiera.

Closes #20

Patch:
diff --git a/hiera/hiera.go b/hiera/hiera.go
index 8378352..9a5f048 100644
--- a/hiera/hiera.go
+++ b/hiera/hiera.go
@@ -4,8 +4,10 @@ import (
 	"context"
 	"fmt"
 	"io"
-
-	"github.com/lyraproj/pcore/utils"
+	"io/ioutil"
+	"os"
+	"regexp"
+	"strings"
 
 	"github.com/lyraproj/hiera/explain"
 	"github.com/lyraproj/hiera/hieraapi"
@@ -14,6 +16,7 @@ import (
 	"github.com/lyraproj/pcore/pcore"
 	"github.com/lyraproj/pcore/px"
 	"github.com/lyraproj/pcore/types"
+	"github.com/lyraproj/pcore/utils"
 	"github.com/lyraproj/pcore/yaml"
 )
 
@@ -29,8 +32,11 @@ type CommandOptions struct {
 	// Default is a pointer to the string representation of a default value or nil if no default value exists
 	Default *string
 
-	// Variables is an optional path to a file containing extra variables to add to the lookup scope
-	Variables string
+	// VarPaths are an optional paths to a files containing extra variables to add to the lookup scope
+	VarPaths []string
+
+	// Variables are an optional paths to a files containing extra variables to add to the lookup scope
+	Variables []string
 
 	// RenderAs is the name of the desired rendering
 	RenderAs string
@@ -78,6 +84,10 @@ func Lookup2(
 	return internal.Lookup2(ic, names, valueType, defaultValue, override, defaultValuesHash, options, block)
 }
 
+// varSplit splits on either ':' or '=' but not on '::', ':=', '=:' or '=='
+var varSplit = regexp.MustCompile(`\A(.*?[^:=])[:=]([^:=].*)\z`)
+var needParsePrefix = []string{`{`, `[`, `"`, `'`}
+
 // LookupAndRender performs a lookup using the given command options and arguments and renders the result on the given
 // io.Writer in accordance with the `RenderAs` option.
 func LookupAndRender(c px.Context, opts *CommandOptions, args []string, out io.Writer) bool {
@@ -101,13 +111,39 @@ func LookupAndRender(c px.Context, opts *CommandOptions, args []string, out io.W
 	}
 
 	scope := px.EmptyMap
-	if opts.Variables != `` {
-		content := types.BinaryFromFile(opts.Variables)
-		yv := yaml.Unmarshal(c, content.Bytes())
+	if vl := len(opts.Variables); vl > 0 {
+		ve := make([]*types.HashEntry, vl)
+		for i, e := range opts.Variables {
+			if m := varSplit.FindStringSubmatch(e); m != nil {
+				key := strings.TrimSpace(m[1])
+				ve[i] = types.WrapHashEntry2(key, parseCommandLineValue(c, key, m[2]))
+			} else {
+				panic(fmt.Errorf("unable to parse variable '%s'", e))
+			}
+		}
+		scope = types.WrapHash(ve)
+	}
+
+	for _, vars := range opts.VarPaths {
+		var content *types.Binary
+		if vars == `-` {
+			data, err := ioutil.ReadAll(os.Stdin)
+			if err != nil {
+				panic(err)
+			}
+			content = types.WrapBinary(data)
+		} else {
+			content = types.BinaryFromFile(vars)
+		}
+		bs := content.Bytes()
+		if len(bs) == 0 {
+			continue
+		}
+		yv := yaml.Unmarshal(c, bs)
 		if data, ok := yv.(px.OrderedMap); ok {
-			scope = data
+			scope = scope.Merge(data)
 		} else {
-			panic(px.Error(hieraapi.YamlNotHash, issue.H{`path`: opts.Variables}))
+			panic(px.Error(hieraapi.YamlNotHash, issue.H{`path`: vars}))
 		}
 	}
 
@@ -142,3 +178,13 @@ func LookupAndRender(c px.Context, opts *CommandOptions, args []string, out io.W
 	Render(c, renderAs, found, out)
 	return true
 }
+
+func parseCommandLineValue(c px.Context, key, vs string) px.Value {
+	vs = strings.TrimSpace(vs)
+	for _, pfx := range needParsePrefix {
+		if strings.HasPrefix(vs, pfx) {
+			return types.ResolveDeferred(c, types.ParseFile(`var `+key, vs), c.Scope())
+		}
+	}
+	return types.WrapString(vs)
+}
diff --git a/hieraserver/rest.go b/hieraserver/rest.go
index 4a263ac..36890c8 100644
--- a/hieraserver/rest.go
+++ b/hieraserver/rest.go
@@ -48,7 +48,8 @@ func newCommand() *cobra.Command {
 	flags := cmd.Flags()
 	flags.StringVar(&logLevel, `loglevel`, `error`, `error/warn/info/debug`)
 	flags.StringVar(&config, `config`, ``, `path to the hiera config file. Overrides <current directory>/hiera.yaml`)
-	flags.StringVar(&cmdOpts.Variables, `variables`, ``, `path to a JSON or YAML file that contains key-value mappings to become scope variables`)
+	flags.StringArrayVar(&cmdOpts.VarPaths, `vars`, nil, `path to a JSON or YAML file that contains key-value mappings to become variables for this lookup`)
+	flags.StringArrayVar(&cmdOpts.Variables, `var`, nil, `variable as a key:value or key=value where value is a literal expressed in Puppet DSL`)
 	flags.IntVar(&port, `port`, 80, `port number to listen to`)
 	return cmd
 }
diff --git a/lookup/lookup.go b/lookup/lookup.go
index 87ae327..eee64bd 100644
--- a/lookup/lookup.go
+++ b/lookup/lookup.go
@@ -5,14 +5,13 @@ import (
 	"fmt"
 	"os"
 
+	"github.com/hashicorp/go-hclog"
+	"github.com/lyraproj/hiera/hiera"
 	"github.com/lyraproj/hiera/hieraapi"
 	"github.com/lyraproj/hiera/provider"
+	"github.com/lyraproj/issue/issue"
 	"github.com/lyraproj/pcore/px"
 	"github.com/lyraproj/pcore/types"
-
-	"github.com/hashicorp/go-hclog"
-	"github.com/lyraproj/hiera/hiera"
-	"github.com/lyraproj/issue/issue"
 	"github.com/spf13/cobra"
 )
 
@@ -70,6 +69,7 @@ var (
 	dflt     OptString
 	logLevel string
 	config   string
+	facts    []string
 )
 
 func main() {
@@ -95,12 +95,14 @@ func newCommand() *cobra.Command {
 	flags.StringVar(&logLevel, `loglevel`, `error`, `error/warn/info/debug`)
 	flags.StringVar(&cmdOpts.Merge, `merge`, `first`, `first/unique/hash/deep`)
 	flags.StringVar(&config, `config`, ``, `path to the hiera config file. Overrides <current directory>/hiera.yaml`)
-	flags.StringVar(&cmdOpts.Variables, `facts`, ``, `path to a JSON or YAML file that contains key-value mappings to become facts for this lookup`)
 	flags.Var(&dflt, `default`, `a value to return if Hiera can't find a value in data`)
 	flags.StringVar(&cmdOpts.Type, `type`, `Any`, `assert that the value has the specified type`)
 	flags.StringVar(&cmdOpts.RenderAs, `render-as`, ``, `s/json/yaml/binary: Specify the output format of the results; s means plain text`)
 	flags.BoolVar(&cmdOpts.ExplainData, `explain`, false, `Explain the details of how the lookup was performed and where the final value came from (or the reason no value was found)`)
 	flags.BoolVar(&cmdOpts.ExplainOptions, `explain-options`, false, `Explain whether a lookup_options hash affects this lookup, and how that hash was assembled`)
+	flags.StringArrayVar(&cmdOpts.VarPaths, `vars`, nil, `path to a JSON or YAML file that contains key-value mappings to become variables for this lookup`)
+	flags.StringArrayVar(&cmdOpts.Variables, `var`, nil, `a key:value or key=value where value is literal expressed using Puppet DSL`)
+	flags.StringArrayVar(&facts, `facts`, nil, `alias for --vars for compatibility with Puppet's ruby version of Hiera`)
 
 	cmd.SetHelpTemplate(helpTemplate)
 	return cmd
@@ -122,6 +124,9 @@ func cmdLookup(cmd *cobra.Command, args []string) {
 	if config != `` {
 		configOptions[hieraapi.HieraConfig] = types.WrapString(config)
 	}
+	if len(facts) > 0 {
+		cmdOpts.VarPaths = append(cmdOpts.VarPaths, facts...)
+	}
 
 	err := hiera.TryWithParent(context.Background(), provider.MuxLookupKey, configOptions, func(c px.Context) error {
 		hiera.LookupAndRender(c, &cmdOpts, args, cmd.OutOrStdout())


Test patch:
diff --git a/lookup/lookup_test.go b/lookup/lookup_test.go
index 72ded1f..29bd392 100644
--- a/lookup/lookup_test.go
+++ b/lookup/lookup_test.go
@@ -75,6 +75,22 @@ func TestLookup_fact_interpolated_config(t *testing.T) {
 	})
 }
 
+func TestLookup_vars_interpolated_config(t *testing.T) {
+	inTestdata(func() {
+		result, err := executeLookup(`--vars`, `facts.yaml`, `interpolate_ca`)
+		require.NoError(t, err)
+		require.Equal(t, "This is value of c.a\n", string(result))
+	})
+}
+
+func TestLookup_var_interpolated_config(t *testing.T) {
+	inTestdata(func() {
+		result, err := executeLookup(`--var`, `c={a=>'the option value'}`, `--var`, `data_file: by_fact`, `interpolate_ca`)
+		require.NoError(t, err)
+		require.Equal(t, "This is the option value\n", string(result))
+	})
+}
+
 func TestLookup_fact_directly(t *testing.T) {
 	inTestdata(func() {
 		result, err := executeLookup(`--facts`, `facts.yaml`, `--config`, `fact_directly.yaml`, `the_fact`)