Skip to content
This repository was archived by the owner on Mar 16, 2025. It is now read-only.

Commit 933da9c

Browse files
committed
add --var flag to define new global variables (closes #1)
1 parent 2a662d5 commit 933da9c

12 files changed

Lines changed: 370 additions & 167 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,9 @@ Rudi comes with a standalone CLI tool called `rudi`.
9090
Usage of rudi:
9191
-i, --interactive Start an interactive REPL to run expressions.
9292
-s, --script string Load Rudi script from file instead of first argument (only in non-interactive mode).
93-
-f, --stdin-format string What data format is used for data provided on stdin, one of [json yaml toml]. (default "yaml")
94-
-o, --output-format string What data format to use for outputting data (if not given, unformatted JSON is used), one of [json yaml toml].
93+
--var stringArray Define additional global variables (can be given multiple times).
94+
-f, --stdin-format string What data format is used for data provided on stdin, one of [raw json yaml toml]. (default "yaml")
95+
-o, --output-format string What data format to use for outputting data, one of [raw json yaml toml]. (default "json")
9596
--enable-funcs Enable the func! function to allow defining new functions in Rudi code.
9697
-c, --coalesce string Type conversion handling, one of [strict pedantic humane]. (default "strict")
9798
-h, --help Show help and documentation.

cmd/rudi/cmd/console/command.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ import (
1111

1212
"go.xrstf.de/rudi"
1313
"go.xrstf.de/rudi/cmd/rudi/docs"
14-
cmdtypes "go.xrstf.de/rudi/cmd/rudi/types"
14+
"go.xrstf.de/rudi/cmd/rudi/options"
1515
"go.xrstf.de/rudi/cmd/rudi/util"
1616
"go.xrstf.de/rudi/pkg/eval/types"
1717

1818
colorjson "github.com/TylerBrock/colorjson"
1919
"github.com/chzyer/readline"
2020
)
2121

22-
func helpCommand(ctx types.Context, opts *cmdtypes.Options) error {
22+
func helpCommand(ctx types.Context, opts *options.Options) error {
2323
content, err := docs.RenderFile("cmd-console.md", nil)
2424
if err != nil {
2525
return err
@@ -41,13 +41,13 @@ func helpTopicCommand(topic string) error {
4141
return nil
4242
}
4343

44-
type replCommandFunc func(ctx types.Context, opts *cmdtypes.Options) error
44+
type replCommandFunc func(ctx types.Context, opts *options.Options) error
4545

4646
var replCommands = map[string]replCommandFunc{
4747
"help": helpCommand,
4848
}
4949

50-
func Run(handler *util.SignalHandler, opts *cmdtypes.Options, args []string, rudiVersion string) error {
50+
func Run(handler *util.SignalHandler, opts *options.Options, args []string, rudiVersion string) error {
5151
rl, err := readline.New("⮞ ")
5252
if err != nil {
5353
return fmt.Errorf("failed to setup readline prompt: %w", err)
@@ -107,7 +107,7 @@ func Run(handler *util.SignalHandler, opts *cmdtypes.Options, args []string, rud
107107
return nil
108108
}
109109

110-
func processInput(handler *util.SignalHandler, rudiCtx types.Context, opts *cmdtypes.Options, input string) (newCtx types.Context, stop bool, err error) {
110+
func processInput(handler *util.SignalHandler, rudiCtx types.Context, opts *options.Options, input string) (newCtx types.Context, stop bool, err error) {
111111
if command, exists := replCommands[input]; exists {
112112
return rudiCtx, false, command(rudiCtx, opts)
113113
}

cmd/rudi/cmd/help/command.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import (
77
"fmt"
88

99
"go.xrstf.de/rudi/cmd/rudi/docs"
10-
"go.xrstf.de/rudi/cmd/rudi/types"
10+
"go.xrstf.de/rudi/cmd/rudi/options"
1111
"go.xrstf.de/rudi/cmd/rudi/util"
1212

1313
"github.com/spf13/pflag"
1414
)
1515

16-
func Run(opts *types.Options, args []string) error {
16+
func Run(opts *options.Options, args []string) error {
1717
// do not show function docs for "--help help if"
1818
if !opts.ShowHelp && len(args) == 2 && args[0] == "help" {
1919
rendered, err := util.RenderHelpTopic(args[1], 0)

cmd/rudi/cmd/script/command.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"strings"
1313

1414
"go.xrstf.de/rudi"
15+
"go.xrstf.de/rudi/cmd/rudi/options"
1516
"go.xrstf.de/rudi/cmd/rudi/types"
1617
"go.xrstf.de/rudi/cmd/rudi/util"
1718
"go.xrstf.de/rudi/pkg/debug"
@@ -20,7 +21,7 @@ import (
2021
"gopkg.in/yaml.v3"
2122
)
2223

23-
func Run(handler *util.SignalHandler, opts *types.Options, args []string) error {
24+
func Run(handler *util.SignalHandler, opts *options.Options, args []string) error {
2425
// determine input script to evaluate
2526
var (
2627
script string
@@ -101,7 +102,7 @@ func Run(handler *util.SignalHandler, opts *types.Options, args []string) error
101102
encoder = toml.NewEncoder(os.Stdout)
102103
encoder.(*toml.Encoder).Indent = " "
103104
default:
104-
encoder = json.NewEncoder(os.Stdout)
105+
encoder = &rawEncoder{}
105106
}
106107

107108
if err := encoder.Encode(evaluated); err != nil {
@@ -110,3 +111,10 @@ func Run(handler *util.SignalHandler, opts *types.Options, args []string) error
110111

111112
return nil
112113
}
114+
115+
type rawEncoder struct{}
116+
117+
func (e *rawEncoder) Encode(v any) error {
118+
fmt.Println(v)
119+
return nil
120+
}

cmd/rudi/encoding/decode.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// SPDX-FileCopyrightText: 2023 Christoph Mewes
2+
// SPDX-License-Identifier: MIT
3+
4+
package encoding
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"io"
10+
11+
"go.xrstf.de/rudi/cmd/rudi/types"
12+
13+
"github.com/BurntSushi/toml"
14+
"gopkg.in/yaml.v3"
15+
)
16+
17+
func Decode(input io.Reader, enc types.Encoding) (any, error) {
18+
var data any
19+
20+
switch enc {
21+
case types.RawEncoding:
22+
content, err := io.ReadAll(input)
23+
if err != nil {
24+
return nil, fmt.Errorf("failed to read input: %w", err)
25+
}
26+
27+
data = string(content)
28+
29+
case types.JsonEncoding:
30+
decoder := json.NewDecoder(input)
31+
if err := decoder.Decode(&data); err != nil {
32+
return nil, fmt.Errorf("failed to parse file as JSON: %w", err)
33+
}
34+
35+
case types.YamlEncoding:
36+
decoder := yaml.NewDecoder(input)
37+
if err := decoder.Decode(&data); err != nil {
38+
return nil, fmt.Errorf("failed to parse file as YAML: %w", err)
39+
}
40+
41+
case types.TomlEncoding:
42+
decoder := toml.NewDecoder(input)
43+
if _, err := decoder.Decode(&data); err != nil {
44+
return nil, fmt.Errorf("failed to parse file as TOML: %w", err)
45+
}
46+
47+
default:
48+
return nil, fmt.Errorf("unexpected encoding %q", enc)
49+
}
50+
51+
return data, nil
52+
}

cmd/rudi/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
"go.xrstf.de/rudi/cmd/rudi/cmd/console"
1616
"go.xrstf.de/rudi/cmd/rudi/cmd/help"
1717
"go.xrstf.de/rudi/cmd/rudi/cmd/script"
18-
"go.xrstf.de/rudi/cmd/rudi/types"
18+
"go.xrstf.de/rudi/cmd/rudi/options"
1919
"go.xrstf.de/rudi/cmd/rudi/util"
2020

2121
"github.com/spf13/pflag"
@@ -86,7 +86,7 @@ func printVersion() {
8686
}
8787

8888
func main() {
89-
opts := types.NewDefaultOptions()
89+
opts := options.NewDefaultOptions()
9090

9191
opts.AddFlags(pflag.CommandLine)
9292
pflag.Parse()

cmd/rudi/options/options.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// SPDX-FileCopyrightText: 2023 Christoph Mewes
2+
// SPDX-License-Identifier: MIT
3+
4+
package options
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
"io"
10+
"os"
11+
"regexp"
12+
"strings"
13+
14+
"go.xrstf.de/rudi/cmd/rudi/encoding"
15+
"go.xrstf.de/rudi/cmd/rudi/types"
16+
17+
"github.com/spf13/pflag"
18+
)
19+
20+
type Options struct {
21+
ShowHelp bool
22+
Interactive bool
23+
ScriptFile string
24+
StdinFormat types.Encoding
25+
OutputFormat types.Encoding
26+
PrintAst bool
27+
ShowVersion bool
28+
Coalescing types.Coalescing
29+
EnableRudispaceFunctions bool
30+
ExtraVariables map[string]any
31+
extraVariableFlags []string
32+
}
33+
34+
func NewDefaultOptions() Options {
35+
return Options{
36+
Coalescing: types.StrictCoalescing,
37+
StdinFormat: types.YamlEncoding,
38+
OutputFormat: types.JsonEncoding,
39+
ExtraVariables: map[string]any{},
40+
}
41+
}
42+
43+
func (o *Options) AddFlags(fs *pflag.FlagSet) {
44+
fs.SortFlags = false
45+
46+
stdinFormatFlag := newEnumFlag(&o.StdinFormat, types.AllEncodings...)
47+
outputFormatFlag := newEnumFlag(&o.OutputFormat, types.AllEncodings...)
48+
coalescingFlag := newEnumFlag(&o.Coalescing, types.AllCoalescings...)
49+
50+
fs.BoolVarP(&o.Interactive, "interactive", "i", o.Interactive, "Start an interactive REPL to run expressions.")
51+
fs.StringVarP(&o.ScriptFile, "script", "s", o.ScriptFile, "Load Rudi script from file instead of first argument (only in non-interactive mode).")
52+
fs.StringArrayVar(&o.extraVariableFlags, "var", o.extraVariableFlags, "Define additional global variables (can be given multiple times).")
53+
stdinFormatFlag.Add(fs, "stdin-format", "f", "What data format is used for data provided on stdin")
54+
outputFormatFlag.Add(fs, "output-format", "o", "What data format to use for outputting data")
55+
fs.BoolVar(&o.EnableRudispaceFunctions, "enable-funcs", o.EnableRudispaceFunctions, "Enable the func! function to allow defining new functions in Rudi code.")
56+
coalescingFlag.Add(fs, "coalesce", "c", "Type conversion handling")
57+
fs.BoolVarP(&o.ShowHelp, "help", "h", o.ShowHelp, "Show help and documentation.")
58+
fs.BoolVarP(&o.ShowVersion, "version", "V", o.ShowVersion, "Show version and exit.")
59+
fs.BoolVarP(&o.PrintAst, "debug-ast", "", o.PrintAst, "Output syntax tree of the parsed script in non-interactive mode.")
60+
}
61+
62+
func (o *Options) Validate() error {
63+
if o.Interactive && o.ScriptFile != "" {
64+
return errors.New("cannot combine --interactive with --script")
65+
}
66+
67+
if o.Interactive && o.PrintAst {
68+
return errors.New("cannot combine --interactive with --debug-ast")
69+
}
70+
71+
if err := o.parseExtraVariables(); err != nil {
72+
return fmt.Errorf("invalid --var flags: %w", err)
73+
}
74+
75+
return nil
76+
}
77+
78+
var extraVariableFlagFormat = regexp.MustCompile(`^([a-zA-Z_][a-zA-Z0-9_]*)=([a-z]+):([a-z]+):(.+)$`)
79+
80+
func (o *Options) parseExtraVariables() error {
81+
for i, flagValue := range o.extraVariableFlags {
82+
varName, value, err := o.parseExtraVariable(flagValue)
83+
if err != nil {
84+
return fmt.Errorf("--var flag %d: %w", i, err)
85+
}
86+
87+
o.ExtraVariables[varName] = value
88+
}
89+
90+
return nil
91+
}
92+
93+
func (o *Options) parseExtraVariable(flagValue string) (string, any, error) {
94+
flagValue = strings.TrimSpace(flagValue)
95+
96+
match := extraVariableFlagFormat.FindStringSubmatch(flagValue)
97+
if match == nil {
98+
return "", nil, errors.New("must be in the form of \"varname=encoding:source:data\"")
99+
}
100+
101+
varName := match[1]
102+
enc := types.Encoding(match[2])
103+
source := types.VariableSource(match[3])
104+
data := match[4]
105+
106+
// validate the given parameters for this variable
107+
108+
if _, exists := o.ExtraVariables[varName]; exists {
109+
return "", nil, fmt.Errorf("variable $%s is defined multiple times", varName)
110+
}
111+
112+
if !enc.IsValid() {
113+
return "", nil, fmt.Errorf("invalid encoding %q, must be one of %v", enc, types.AllEncodings)
114+
}
115+
116+
if !source.IsValid() {
117+
return "", nil, fmt.Errorf("invalid source type %q, must be one of %v", source, types.AllVariableSources)
118+
}
119+
120+
// resolve the variable source
121+
122+
var input io.Reader
123+
124+
switch source {
125+
case types.StringVariableSource:
126+
input = strings.NewReader(data)
127+
case types.EnvironmentVariableSource:
128+
input = strings.NewReader(os.Getenv(data))
129+
case types.FileVariableSource:
130+
f, err := os.Open(data)
131+
if err != nil {
132+
return "", nil, fmt.Errorf("failed to open %q: %w", data, err)
133+
}
134+
defer f.Close()
135+
136+
input = f
137+
default:
138+
// This should never happen.
139+
return "", nil, fmt.Errorf("unknown source type %q", source)
140+
}
141+
142+
// parse the data as requested
143+
144+
varData, err := encoding.Decode(input, enc)
145+
if err != nil {
146+
return "", nil, fmt.Errorf("failed to decode data: %w", err)
147+
}
148+
149+
return varName, varData, nil
150+
}

0 commit comments

Comments
 (0)