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

Commit db8ac68

Browse files
committed
add --library flag to load additional Rudi scripts
1 parent ea75efe commit db8ac68

7 files changed

Lines changed: 137 additions & 25 deletions

File tree

cmd/rudi/cmd/console/command.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ var replCommands = map[string]replCommandFunc{
4747
"help": helpCommand,
4848
}
4949

50-
func Run(handler *util.SignalHandler, opts *options.Options, args []string, rudiVersion string) error {
50+
func Run(handler *util.SignalHandler, opts *options.Options, library rudi.Program, 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)
@@ -67,6 +67,15 @@ func Run(handler *util.SignalHandler, opts *options.Options, args []string, rudi
6767
fmt.Println("Type `help` for more information, `exit` or Ctrl-D to exit, Ctrl-C to interrupt statements.")
6868
fmt.Println("")
6969

70+
// Evaluate the library (its return value is irrelevant, as the main program has to have
71+
// at least 1 statement, which will overwrite the total return value anyway).
72+
if library != nil {
73+
_, err = runProgram(handler, rudiCtx, library)
74+
if err != nil {
75+
return fmt.Errorf("failed to evaluate library: %w", err)
76+
}
77+
}
78+
7079
for {
7180
line, err := rl.Readline()
7281

@@ -125,13 +134,8 @@ func processInput(handler *util.SignalHandler, rudiCtx types.Context, opts *opti
125134
return false, err
126135
}
127136

128-
// allow to interrupt the statement
129-
ctx, cancel := context.WithCancel(context.Background())
130-
defer cancel()
131-
132-
handler.SetCancelFn(cancel)
133-
134-
evaluated, err := program.RunContext(rudiCtx.WithGoContext(ctx))
137+
// run the program
138+
evaluated, err := runProgram(handler, rudiCtx, program)
135139
if err != nil {
136140
return false, err
137141
}
@@ -149,3 +153,13 @@ func processInput(handler *util.SignalHandler, rudiCtx types.Context, opts *opti
149153

150154
return false, nil
151155
}
156+
157+
func runProgram(handler *util.SignalHandler, rudiCtx types.Context, prog rudi.Program) (any, error) {
158+
// allow to interrupt the statement
159+
ctx, cancel := context.WithCancel(context.Background())
160+
defer cancel()
161+
162+
handler.SetCancelFn(cancel)
163+
164+
return prog.RunContext(rudiCtx.WithGoContext(ctx))
165+
}

cmd/rudi/cmd/script/command.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
"go.xrstf.de/rudi/cmd/rudi/util"
1717
)
1818

19-
func Run(handler *util.SignalHandler, opts *options.Options, args []string) error {
19+
func Run(handler *util.SignalHandler, opts *options.Options, library rudi.Program, args []string) error {
2020
// determine input script to evaluate
2121
var (
2222
script string
@@ -39,7 +39,7 @@ func Run(handler *util.SignalHandler, opts *options.Options, args []string) erro
3939
// consume one arg for the script
4040
script = args[0]
4141
args = args[1:]
42-
scriptName = "(stdin)"
42+
scriptName = "(cli)"
4343
}
4444

4545
// parse the script
@@ -75,6 +75,15 @@ func Run(handler *util.SignalHandler, opts *options.Options, args []string) erro
7575

7676
handler.SetCancelFn(cancel)
7777

78+
// Evaluate the library (its return value is irrelevant, as the main program has to have
79+
// at least 1 statement, which will overwrite the total return value anyway).
80+
if library != nil {
81+
_, err = library.RunContext(rudiCtx.WithGoContext(subCtx))
82+
if err != nil {
83+
return fmt.Errorf("failed to evaluate script: %w", err)
84+
}
85+
}
86+
7887
// evaluate the script
7988
evaluated, err := program.RunContext(rudiCtx.WithGoContext(subCtx))
8089
if err != nil {

cmd/rudi/encoding/decode.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,26 @@ import (
1616
"gopkg.in/yaml.v3"
1717
)
1818

19+
func decodeYaml(input io.Reader) ([]any, error) {
20+
decoder := yaml.NewDecoder(input)
21+
22+
documents := []any{}
23+
for {
24+
var doc any
25+
if err := decoder.Decode(&doc); err != nil {
26+
if errors.Is(err, io.EOF) {
27+
break
28+
}
29+
30+
return nil, fmt.Errorf("failed to parse file as YAML: %w", err)
31+
}
32+
33+
documents = append(documents, doc)
34+
}
35+
36+
return documents, nil
37+
}
38+
1939
func Decode(input io.Reader, enc types.Encoding) (any, error) {
2040
var data any
2141

@@ -41,6 +61,21 @@ func Decode(input io.Reader, enc types.Encoding) (any, error) {
4161
}
4262

4363
case types.YamlEncoding:
64+
decoded, err := decodeYaml(input)
65+
if err != nil {
66+
return nil, fmt.Errorf("failed to parse file as JSON5: %w", err)
67+
}
68+
69+
switch len(decoded) {
70+
case 0:
71+
data = nil
72+
case 1:
73+
data = decoded[0]
74+
default:
75+
data = decoded
76+
}
77+
78+
case types.YamlDocumentsEncoding:
4479
decoder := yaml.NewDecoder(input)
4580

4681
documents := []any{}
@@ -57,11 +92,7 @@ func Decode(input io.Reader, enc types.Encoding) (any, error) {
5792
documents = append(documents, doc)
5893
}
5994

60-
if len(documents) == 1 {
61-
data = documents[0]
62-
} else {
63-
data = documents
64-
}
95+
data = documents
6596

6697
case types.TomlEncoding:
6798
decoder := toml.NewDecoder(input)

cmd/rudi/main.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os"
1010
"runtime"
1111
"runtime/debug"
12+
"strings"
1213

1314
"go.xrstf.de/rudi"
1415
"go.xrstf.de/rudi/cmd/rudi/batteries"
@@ -114,16 +115,42 @@ func main() {
114115

115116
handler := util.SetupSignalHandler()
116117

117-
if opts.Interactive || len(args) == 0 {
118-
if err := console.Run(handler, &opts, args, BuildTag); err != nil {
118+
// load all --library files and assemble a single base script for both console/script mode
119+
baseScripts := []string{}
120+
for _, filename := range opts.LibraryFiles {
121+
_, script, err := util.ParseFile(filename)
122+
if err != nil {
123+
fmt.Printf("Error: library %q: %v\n", filename, err)
124+
os.Exit(1)
125+
}
126+
127+
baseScripts = append(baseScripts, script)
128+
}
129+
130+
// parse the base script
131+
var baseProgram rudi.Program
132+
133+
if len(baseScripts) > 0 {
134+
var err error
135+
136+
baseProgram, err = rudi.Parse("(library)", strings.Join(baseScripts, "\n"))
137+
if err != nil {
138+
// This should never happen, each script was already syntax-checked.
139+
fmt.Printf("Error: library: %v\n", err)
140+
os.Exit(1)
141+
}
142+
}
143+
144+
if opts.Interactive {
145+
if err := console.Run(handler, &opts, baseProgram, args, BuildTag); err != nil {
119146
fmt.Printf("Error: %v\n", err)
120147
os.Exit(1)
121148
}
122149

123150
return
124151
}
125152

126-
if err := script.Run(handler, &opts, args); err != nil {
153+
if err := script.Run(handler, &opts, baseProgram, args); err != nil {
127154
parseErr := &rudi.ParseError{}
128155
if errors.As(err, parseErr) {
129156
fmt.Println(parseErr.Snippet())

cmd/rudi/options/options.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type Options struct {
2121
ShowHelp bool
2222
Interactive bool
2323
ScriptFile string
24+
LibraryFiles []string
2425
StdinFormat types.Encoding
2526
OutputFormat types.Encoding
2627
PrintAst bool
@@ -49,6 +50,7 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) {
4950

5051
fs.BoolVarP(&o.Interactive, "interactive", "i", o.Interactive, "Start an interactive REPL to run expressions.")
5152
fs.StringVarP(&o.ScriptFile, "script", "s", o.ScriptFile, "Load Rudi script from file instead of first argument (only in non-interactive mode).")
53+
fs.StringArrayVarP(&o.LibraryFiles, "library", "l", o.LibraryFiles, "Load additional Rudi file(s) to be be evaluated before the script (can be given multiple times).")
5254
fs.StringArrayVar(&o.extraVariableFlags, "var", o.extraVariableFlags, "Define additional global variables (can be given multiple times).")
5355
stdinFormatFlag.Add(fs, "stdin-format", "f", "What data format is used for data provided on stdin")
5456
outputFormatFlag.Add(fs, "output-format", "o", "What data format to use for outputting data")
@@ -60,10 +62,6 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) {
6062
}
6163

6264
func (o *Options) Validate() error {
63-
if o.Interactive && o.ScriptFile != "" {
64-
return errors.New("cannot combine --interactive with --script")
65-
}
66-
6765
if o.Interactive && o.PrintAst {
6866
return errors.New("cannot combine --interactive with --debug-ast")
6967
}
@@ -72,6 +70,10 @@ func (o *Options) Validate() error {
7270
return fmt.Errorf("invalid --var flags: %w", err)
7371
}
7472

73+
if err := o.validateLibraryFiles(); err != nil {
74+
return fmt.Errorf("invalid --library flags: %w", err)
75+
}
76+
7577
return nil
7678
}
7779

@@ -148,3 +150,17 @@ func (o *Options) parseExtraVariable(flagValue string) (string, any, error) {
148150

149151
return varName, varData, nil
150152
}
153+
154+
func (o *Options) validateLibraryFiles() error {
155+
for _, file := range o.LibraryFiles {
156+
info, err := os.Stat(file)
157+
if err != nil {
158+
return fmt.Errorf("invalid library %q: %w", file, err)
159+
}
160+
if info.IsDir() {
161+
return fmt.Errorf("invalid library %q: is directory", file)
162+
}
163+
}
164+
165+
return nil
166+
}

cmd/rudi/types/const.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,12 @@ var (
4141
TomlEncoding,
4242
}
4343

44-
// InputEncodings contains all valid encodings for reading data.
45-
// Note that YAML is always read in multi-document mode, hence
46-
// YamlDocumentsEncoding is not part of this list.
4744
InputEncodings = []Encoding{
4845
RawEncoding,
4946
JsonEncoding,
5047
Json5Encoding,
5148
YamlEncoding,
49+
YamlDocumentsEncoding,
5250
TomlEncoding,
5351
}
5452

cmd/rudi/util/files.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,28 @@ import (
1010
"path/filepath"
1111
"strings"
1212

13+
"go.xrstf.de/rudi"
1314
"go.xrstf.de/rudi/cmd/rudi/encoding"
1415
"go.xrstf.de/rudi/cmd/rudi/options"
1516
"go.xrstf.de/rudi/cmd/rudi/types"
1617
)
1718

19+
func ParseFile(filename string) (rudi.Program, string, error) {
20+
content, err := os.ReadFile(filename)
21+
if err != nil {
22+
return nil, "", fmt.Errorf("failed to read script: %w", err)
23+
}
24+
25+
script := strings.TrimSpace(string(content))
26+
27+
prog, err := rudi.Parse(filename, script)
28+
if err != nil {
29+
return nil, "", fmt.Errorf("failed to parse: %w", err)
30+
}
31+
32+
return prog, script, nil
33+
}
34+
1835
func LoadFiles(opts *options.Options, filenames []string) ([]any, error) {
1936
results := make([]any, len(filenames))
2037

0 commit comments

Comments
 (0)