From 78720eeb5b671a2446b71e5385477cd62a2ed4ce Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sat, 4 May 2024 10:59:17 +1000 Subject: [PATCH] Started working on builtins --- cmd/cmsh/main.go | 6 ++++- ucl/ast.go | 2 +- ucl/builtins/fs.go | 56 +++++++++++++++++++++++++++++++++++++++++ ucl/builtins/fs_test.go | 37 +++++++++++++++++++++++++++ ucl/builtins/os.go | 40 +++++++++++++++++++++++++++++ ucl/builtins/os_test.go | 36 ++++++++++++++++++++++++++ ucl/evaldisplay.go | 6 +++++ ucl/inst.go | 13 ++++++++++ ucl/userbuiltin.go | 2 ++ 9 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 ucl/builtins/fs.go create mode 100644 ucl/builtins/fs_test.go create mode 100644 ucl/builtins/os.go create mode 100644 ucl/builtins/os_test.go diff --git a/cmd/cmsh/main.go b/cmd/cmsh/main.go index d96aebd..afd1ba5 100644 --- a/cmd/cmsh/main.go +++ b/cmd/cmsh/main.go @@ -5,6 +5,7 @@ import ( "github.com/chzyer/readline" "log" "ucl.lmika.dev/ucl" + "ucl.lmika.dev/ucl/builtins" ) func main() { @@ -14,7 +15,10 @@ func main() { } defer rl.Close() - inst := ucl.New() + inst := ucl.New( + ucl.WithModule(builtins.OS()), + ucl.WithModule(builtins.FS(nil)), + ) ctx := context.Background() for { diff --git a/ucl/ast.go b/ucl/ast.go index ce2bf2f..79e9569 100644 --- a/ucl/ast.go +++ b/ucl/ast.go @@ -83,7 +83,7 @@ var scanner = lexer.MustStateful(lexer.Rules{ {"RC", `\}`, nil}, {"NL", `[;\n][; \n\t]*`, nil}, {"PIPE", `\|`, nil}, - {"Ident", `[-]*[a-zA-Z_][\w-]*`, nil}, + {"Ident", `[-]*[a-zA-Z_:][\w-:]*`, nil}, }, }) var parser = participle.MustBuild[astScript](participle.Lexer(scanner), diff --git a/ucl/builtins/fs.go b/ucl/builtins/fs.go new file mode 100644 index 0000000..6d35eac --- /dev/null +++ b/ucl/builtins/fs.go @@ -0,0 +1,56 @@ +package builtins + +import ( + "bufio" + "context" + "io/fs" + "os" + "ucl.lmika.dev/ucl" +) + +type fsHandlers struct { + fs fs.FS +} + +func FS(fs fs.FS) ucl.Module { + fsh := fsHandlers{fs: fs} + + return ucl.Module{ + Name: "fs", + Builtins: map[string]ucl.BuiltinHandler{ + "lines": fsh.lines, + }, + } +} + +func (fh fsHandlers) openFile(name string) (fs.File, error) { + if fh.fs == nil { + return os.Open(name) + } + return fh.fs.Open(name) +} + +func (fh fsHandlers) lines(ctx context.Context, args ucl.CallArgs) (any, error) { + var fname string + if err := args.Bind(&fname); err != nil { + return nil, err + } + + f, err := fh.openFile(fname) + if err != nil { + return nil, err + } + defer f.Close() + + lines := make([]string, 0) + scanner := bufio.NewScanner(f) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return lines, nil +} diff --git a/ucl/builtins/fs_test.go b/ucl/builtins/fs_test.go new file mode 100644 index 0000000..f7f0e41 --- /dev/null +++ b/ucl/builtins/fs_test.go @@ -0,0 +1,37 @@ +package builtins_test + +import ( + "context" + "github.com/stretchr/testify/assert" + "testing" + "testing/fstest" + "ucl.lmika.dev/ucl" + "ucl.lmika.dev/ucl/builtins" +) + +var testFS = fstest.MapFS{ + "test.txt": &fstest.MapFile{ + Data: []byte("these\nare\nlines"), + }, +} + +func TestFS_Cat(t *testing.T) { + tests := []struct { + descr string + eval string + want any + }{ + {descr: "read file", eval: `fs:lines "test.txt"`, want: []string{"these", "are", "lines"}}, + } + + for _, tt := range tests { + t.Run(tt.descr, func(t *testing.T) { + inst := ucl.New( + ucl.WithModule(builtins.FS(testFS)), + ) + res, err := inst.Eval(context.Background(), tt.eval) + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + }) + } +} diff --git a/ucl/builtins/os.go b/ucl/builtins/os.go new file mode 100644 index 0000000..e51f62f --- /dev/null +++ b/ucl/builtins/os.go @@ -0,0 +1,40 @@ +package builtins + +import ( + "context" + "os" + "ucl.lmika.dev/ucl" +) + +type osHandlers struct { +} + +func OS() ucl.Module { + osh := osHandlers{} + + return ucl.Module{ + Name: "os", + Builtins: map[string]ucl.BuiltinHandler{ + "env": osh.env, + }, + } +} + +func (oh osHandlers) env(ctx context.Context, args ucl.CallArgs) (any, error) { + var envName string + if err := args.Bind(&envName); err != nil { + return nil, err + } + + val, ok := os.LookupEnv(envName) + if ok { + return val, nil + } + + var defValue any + if err := args.Bind(&defValue); err == nil { + return defValue, nil + } + + return "", nil +} diff --git a/ucl/builtins/os_test.go b/ucl/builtins/os_test.go new file mode 100644 index 0000000..a246892 --- /dev/null +++ b/ucl/builtins/os_test.go @@ -0,0 +1,36 @@ +package builtins_test + +import ( + "context" + "github.com/stretchr/testify/assert" + "testing" + "ucl.lmika.dev/ucl" + "ucl.lmika.dev/ucl/builtins" +) + +func TestOS_Env(t *testing.T) { + tests := []struct { + descr string + eval string + want any + }{ + {descr: "env value", eval: `os:env "MY_ENV"`, want: "my env value"}, + {descr: "missing env value", eval: `os:env "MISSING_THING"`, want: ""}, + {descr: "default env value (str)", eval: `os:env "MISSING_THING" "my default"`, want: "my default"}, + {descr: "default env value (int)", eval: `os:env "MISSING_THING" 1352`, want: 1352}, + {descr: "default env value (nil)", eval: `os:env "MISSING_THING" ()`, want: nil}, + } + + for _, tt := range tests { + t.Run(tt.descr, func(t *testing.T) { + t.Setenv("MY_ENV", "my env value") + + inst := ucl.New( + ucl.WithModule(builtins.OS()), + ) + res, err := inst.Eval(context.Background(), tt.eval) + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + }) + } +} diff --git a/ucl/evaldisplay.go b/ucl/evaldisplay.go index 32e2002..5b4a209 100644 --- a/ucl/evaldisplay.go +++ b/ucl/evaldisplay.go @@ -20,6 +20,12 @@ func displayResult(ctx context.Context, inst *Inst, res object) (err error) { if _, err = fmt.Fprintln(inst.out, "(nil)"); err != nil { return err } + case listable: + for i := 0; i < v.Len(); i++ { + if err = displayResult(ctx, inst, v.Index(i)); err != nil { + return err + } + } default: if _, err = fmt.Fprintln(inst.out, v.String()); err != nil { return err diff --git a/ucl/inst.go b/ucl/inst.go index 8fa9561..61864dc 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -29,6 +29,19 @@ func WithMissingBuiltinHandler(handler MissingBuiltinHandler) InstOption { } } +func WithModule(module Module) InstOption { + return func(i *Inst) { + for name, builtin := range module.Builtins { + i.SetBuiltin(module.Name+":"+name, builtin) + } + } +} + +type Module struct { + Name string + Builtins map[string]BuiltinHandler +} + func New(opts ...InstOption) *Inst { rootEC := &evalCtx{} rootEC.root = rootEC diff --git a/ucl/userbuiltin.go b/ucl/userbuiltin.go index 2477308..591a73d 100644 --- a/ucl/userbuiltin.go +++ b/ucl/userbuiltin.go @@ -94,6 +94,8 @@ func (u userBuiltin) invoke(ctx context.Context, args invocationArgs) (object, e func bindArg(v interface{}, arg object) error { switch t := v.(type) { + case *interface{}: + *t, _ = toGoValue(arg) case *string: *t = arg.String() case *int: