Skip to content

Commit

Permalink
add --var flag to define new global variables (closes #1)
Browse files Browse the repository at this point in the history
  • Loading branch information
xrstf committed Dec 22, 2023
1 parent 2a662d5 commit 933da9c
Show file tree
Hide file tree
Showing 12 changed files with 370 additions and 167 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ Rudi comes with a standalone CLI tool called `rudi`.
Usage of rudi:
-i, --interactive Start an interactive REPL to run expressions.
-s, --script string Load Rudi script from file instead of first argument (only in non-interactive mode).
-f, --stdin-format string What data format is used for data provided on stdin, one of [json yaml toml]. (default "yaml")
-o, --output-format string What data format to use for outputting data (if not given, unformatted JSON is used), one of [json yaml toml].
--var stringArray Define additional global variables (can be given multiple times).
-f, --stdin-format string What data format is used for data provided on stdin, one of [raw json yaml toml]. (default "yaml")
-o, --output-format string What data format to use for outputting data, one of [raw json yaml toml]. (default "json")
--enable-funcs Enable the func! function to allow defining new functions in Rudi code.
-c, --coalesce string Type conversion handling, one of [strict pedantic humane]. (default "strict")
-h, --help Show help and documentation.
Expand Down
10 changes: 5 additions & 5 deletions cmd/rudi/cmd/console/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import (

"go.xrstf.de/rudi"
"go.xrstf.de/rudi/cmd/rudi/docs"
cmdtypes "go.xrstf.de/rudi/cmd/rudi/types"
"go.xrstf.de/rudi/cmd/rudi/options"
"go.xrstf.de/rudi/cmd/rudi/util"
"go.xrstf.de/rudi/pkg/eval/types"

colorjson "github.com/TylerBrock/colorjson"
"github.com/chzyer/readline"
)

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

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

var replCommands = map[string]replCommandFunc{
"help": helpCommand,
}

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

func processInput(handler *util.SignalHandler, rudiCtx types.Context, opts *cmdtypes.Options, input string) (newCtx types.Context, stop bool, err error) {
func processInput(handler *util.SignalHandler, rudiCtx types.Context, opts *options.Options, input string) (newCtx types.Context, stop bool, err error) {
if command, exists := replCommands[input]; exists {
return rudiCtx, false, command(rudiCtx, opts)
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/rudi/cmd/help/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import (
"fmt"

"go.xrstf.de/rudi/cmd/rudi/docs"
"go.xrstf.de/rudi/cmd/rudi/types"
"go.xrstf.de/rudi/cmd/rudi/options"
"go.xrstf.de/rudi/cmd/rudi/util"

"github.com/spf13/pflag"
)

func Run(opts *types.Options, args []string) error {
func Run(opts *options.Options, args []string) error {
// do not show function docs for "--help help if"
if !opts.ShowHelp && len(args) == 2 && args[0] == "help" {
rendered, err := util.RenderHelpTopic(args[1], 0)
Expand Down
12 changes: 10 additions & 2 deletions cmd/rudi/cmd/script/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"strings"

"go.xrstf.de/rudi"
"go.xrstf.de/rudi/cmd/rudi/options"
"go.xrstf.de/rudi/cmd/rudi/types"
"go.xrstf.de/rudi/cmd/rudi/util"
"go.xrstf.de/rudi/pkg/debug"
Expand All @@ -20,7 +21,7 @@ import (
"gopkg.in/yaml.v3"
)

func Run(handler *util.SignalHandler, opts *types.Options, args []string) error {
func Run(handler *util.SignalHandler, opts *options.Options, args []string) error {
// determine input script to evaluate
var (
script string
Expand Down Expand Up @@ -101,7 +102,7 @@ func Run(handler *util.SignalHandler, opts *types.Options, args []string) error
encoder = toml.NewEncoder(os.Stdout)
encoder.(*toml.Encoder).Indent = " "
default:
encoder = json.NewEncoder(os.Stdout)
encoder = &rawEncoder{}
}

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

return nil
}

type rawEncoder struct{}

func (e *rawEncoder) Encode(v any) error {
fmt.Println(v)
return nil
}
52 changes: 52 additions & 0 deletions cmd/rudi/encoding/decode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: 2023 Christoph Mewes
// SPDX-License-Identifier: MIT

package encoding

import (
"encoding/json"
"fmt"
"io"

"go.xrstf.de/rudi/cmd/rudi/types"

"github.com/BurntSushi/toml"
"gopkg.in/yaml.v3"
)

func Decode(input io.Reader, enc types.Encoding) (any, error) {
var data any

switch enc {
case types.RawEncoding:
content, err := io.ReadAll(input)
if err != nil {
return nil, fmt.Errorf("failed to read input: %w", err)
}

data = string(content)

case types.JsonEncoding:
decoder := json.NewDecoder(input)
if err := decoder.Decode(&data); err != nil {
return nil, fmt.Errorf("failed to parse file as JSON: %w", err)
}

case types.YamlEncoding:
decoder := yaml.NewDecoder(input)
if err := decoder.Decode(&data); err != nil {
return nil, fmt.Errorf("failed to parse file as YAML: %w", err)
}

case types.TomlEncoding:
decoder := toml.NewDecoder(input)
if _, err := decoder.Decode(&data); err != nil {
return nil, fmt.Errorf("failed to parse file as TOML: %w", err)
}

default:
return nil, fmt.Errorf("unexpected encoding %q", enc)
}

return data, nil
}
4 changes: 2 additions & 2 deletions cmd/rudi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"go.xrstf.de/rudi/cmd/rudi/cmd/console"
"go.xrstf.de/rudi/cmd/rudi/cmd/help"
"go.xrstf.de/rudi/cmd/rudi/cmd/script"
"go.xrstf.de/rudi/cmd/rudi/types"
"go.xrstf.de/rudi/cmd/rudi/options"
"go.xrstf.de/rudi/cmd/rudi/util"

"github.com/spf13/pflag"
Expand Down Expand Up @@ -86,7 +86,7 @@ func printVersion() {
}

func main() {
opts := types.NewDefaultOptions()
opts := options.NewDefaultOptions()

opts.AddFlags(pflag.CommandLine)
pflag.Parse()
Expand Down
150 changes: 150 additions & 0 deletions cmd/rudi/options/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// SPDX-FileCopyrightText: 2023 Christoph Mewes
// SPDX-License-Identifier: MIT

package options

import (
"errors"
"fmt"
"io"
"os"
"regexp"
"strings"

"go.xrstf.de/rudi/cmd/rudi/encoding"
"go.xrstf.de/rudi/cmd/rudi/types"

"github.com/spf13/pflag"
)

type Options struct {
ShowHelp bool
Interactive bool
ScriptFile string
StdinFormat types.Encoding
OutputFormat types.Encoding
PrintAst bool
ShowVersion bool
Coalescing types.Coalescing
EnableRudispaceFunctions bool
ExtraVariables map[string]any
extraVariableFlags []string
}

func NewDefaultOptions() Options {
return Options{
Coalescing: types.StrictCoalescing,
StdinFormat: types.YamlEncoding,
OutputFormat: types.JsonEncoding,
ExtraVariables: map[string]any{},
}
}

func (o *Options) AddFlags(fs *pflag.FlagSet) {
fs.SortFlags = false

stdinFormatFlag := newEnumFlag(&o.StdinFormat, types.AllEncodings...)
outputFormatFlag := newEnumFlag(&o.OutputFormat, types.AllEncodings...)
coalescingFlag := newEnumFlag(&o.Coalescing, types.AllCoalescings...)

fs.BoolVarP(&o.Interactive, "interactive", "i", o.Interactive, "Start an interactive REPL to run expressions.")
fs.StringVarP(&o.ScriptFile, "script", "s", o.ScriptFile, "Load Rudi script from file instead of first argument (only in non-interactive mode).")
fs.StringArrayVar(&o.extraVariableFlags, "var", o.extraVariableFlags, "Define additional global variables (can be given multiple times).")
stdinFormatFlag.Add(fs, "stdin-format", "f", "What data format is used for data provided on stdin")
outputFormatFlag.Add(fs, "output-format", "o", "What data format to use for outputting data")
fs.BoolVar(&o.EnableRudispaceFunctions, "enable-funcs", o.EnableRudispaceFunctions, "Enable the func! function to allow defining new functions in Rudi code.")
coalescingFlag.Add(fs, "coalesce", "c", "Type conversion handling")
fs.BoolVarP(&o.ShowHelp, "help", "h", o.ShowHelp, "Show help and documentation.")
fs.BoolVarP(&o.ShowVersion, "version", "V", o.ShowVersion, "Show version and exit.")
fs.BoolVarP(&o.PrintAst, "debug-ast", "", o.PrintAst, "Output syntax tree of the parsed script in non-interactive mode.")
}

func (o *Options) Validate() error {
if o.Interactive && o.ScriptFile != "" {
return errors.New("cannot combine --interactive with --script")
}

if o.Interactive && o.PrintAst {
return errors.New("cannot combine --interactive with --debug-ast")
}

if err := o.parseExtraVariables(); err != nil {
return fmt.Errorf("invalid --var flags: %w", err)
}

return nil
}

var extraVariableFlagFormat = regexp.MustCompile(`^([a-zA-Z_][a-zA-Z0-9_]*)=([a-z]+):([a-z]+):(.+)$`)

func (o *Options) parseExtraVariables() error {
for i, flagValue := range o.extraVariableFlags {
varName, value, err := o.parseExtraVariable(flagValue)
if err != nil {
return fmt.Errorf("--var flag %d: %w", i, err)
}

o.ExtraVariables[varName] = value
}

return nil
}

func (o *Options) parseExtraVariable(flagValue string) (string, any, error) {
flagValue = strings.TrimSpace(flagValue)

match := extraVariableFlagFormat.FindStringSubmatch(flagValue)
if match == nil {
return "", nil, errors.New("must be in the form of \"varname=encoding:source:data\"")
}

varName := match[1]
enc := types.Encoding(match[2])
source := types.VariableSource(match[3])
data := match[4]

// validate the given parameters for this variable

if _, exists := o.ExtraVariables[varName]; exists {
return "", nil, fmt.Errorf("variable $%s is defined multiple times", varName)
}

if !enc.IsValid() {
return "", nil, fmt.Errorf("invalid encoding %q, must be one of %v", enc, types.AllEncodings)
}

if !source.IsValid() {
return "", nil, fmt.Errorf("invalid source type %q, must be one of %v", source, types.AllVariableSources)
}

// resolve the variable source

var input io.Reader

switch source {
case types.StringVariableSource:
input = strings.NewReader(data)
case types.EnvironmentVariableSource:
input = strings.NewReader(os.Getenv(data))
case types.FileVariableSource:
f, err := os.Open(data)
if err != nil {
return "", nil, fmt.Errorf("failed to open %q: %w", data, err)
}
defer f.Close()

input = f
default:
// This should never happen.
return "", nil, fmt.Errorf("unknown source type %q", source)
}

// parse the data as requested

varData, err := encoding.Decode(input, enc)
if err != nil {
return "", nil, fmt.Errorf("failed to decode data: %w", err)
}

return varName, varData, nil
}

0 comments on commit 933da9c

Please sign in to comment.