This commit is contained in:
parent
e7f904e7da
commit
0f1ceba090
|
@ -86,6 +86,7 @@ type astCmdArg struct {
|
|||
Literal *astLiteral `parser:"@@"`
|
||||
Ident *astIdentNames `parser:"| @@"`
|
||||
Var *string `parser:"| DOLLAR @Ident"`
|
||||
PseudoVar *string `parser:"| AT @Ident"`
|
||||
MaybeSub *astMaybeSub `parser:"| LP @@ RP"`
|
||||
ListOrHash *astListOrHash `parser:"| @@"`
|
||||
Block *astBlock `parser:"| @@"`
|
||||
|
@ -129,6 +130,7 @@ var scanner = lexer.MustStateful(lexer.Rules{
|
|||
{"SingleStringStart", `'`, lexer.Push("SingleString")},
|
||||
{"Int", `[-]?[0-9][0-9]*`, nil},
|
||||
{"DOLLAR", `\$`, nil},
|
||||
{"AT", `@`, nil},
|
||||
{"COLON", `\:`, nil},
|
||||
{"DOT", `[.]`, nil},
|
||||
{"LP", `\(`, lexer.Push("Root")},
|
||||
|
|
|
@ -180,6 +180,7 @@ func modBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
|
|||
return IntObject(n), nil
|
||||
}
|
||||
|
||||
// TODO: this may need to be a macro
|
||||
func setBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
|
||||
if err := args.expectArgn(2); err != nil {
|
||||
return nil, err
|
||||
|
@ -188,10 +189,32 @@ func setBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
|
|||
name, err := args.stringArg(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if len(name) == 0 {
|
||||
return nil, fmt.Errorf("attempt to set empty string")
|
||||
}
|
||||
|
||||
newVal := args.args[1]
|
||||
|
||||
if strings.HasPrefix(name, "@") {
|
||||
pname := name[1:]
|
||||
pvar, ok := args.ec.getPseudoVar(pname)
|
||||
if ok {
|
||||
if err := pvar.set(ctx, pname, newVal); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newVal, nil
|
||||
}
|
||||
|
||||
if pvar := args.inst.missingPseudoVarHandler; pvar != nil {
|
||||
if err := pvar.set(ctx, pname, newVal); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newVal, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("attempt to set '%v' to a non-existent pseudo-variable", name)
|
||||
}
|
||||
|
||||
args.ec.setOrDefineVar(name, newVal)
|
||||
return newVal, nil
|
||||
}
|
||||
|
|
19
ucl/env.go
19
ucl/env.go
|
@ -1,11 +1,12 @@
|
|||
package ucl
|
||||
|
||||
type evalCtx struct {
|
||||
root *evalCtx
|
||||
parent *evalCtx
|
||||
commands map[string]invokable
|
||||
macros map[string]macroable
|
||||
vars map[string]Object
|
||||
root *evalCtx
|
||||
parent *evalCtx
|
||||
commands map[string]invokable
|
||||
macros map[string]macroable
|
||||
vars map[string]Object
|
||||
pseudoVars map[string]pseudoVar
|
||||
}
|
||||
|
||||
func (ec *evalCtx) forkAndIsolate() *evalCtx {
|
||||
|
@ -72,6 +73,14 @@ func (ec *evalCtx) getVar(name string) (Object, bool) {
|
|||
return nil, false
|
||||
}
|
||||
|
||||
func (ec *evalCtx) getPseudoVar(name string) (pseudoVar, bool) {
|
||||
if ec.root.pseudoVars == nil {
|
||||
return nil, false
|
||||
}
|
||||
pvar, ok := ec.root.pseudoVars[name]
|
||||
return pvar, ok
|
||||
}
|
||||
|
||||
func (ec *evalCtx) lookupInvokable(name string) invokable {
|
||||
if ec == nil {
|
||||
return nil
|
||||
|
|
15
ucl/eval.go
15
ucl/eval.go
|
@ -187,6 +187,16 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Objec
|
|||
return v, nil
|
||||
}
|
||||
return nil, nil
|
||||
case n.PseudoVar != nil:
|
||||
if v, ok := ec.getPseudoVar(*n.PseudoVar); ok {
|
||||
return v.get(ctx, *n.PseudoVar)
|
||||
}
|
||||
|
||||
if mph := e.inst.missingPseudoVarHandler; mph != nil {
|
||||
return mph.get(ctx, *n.PseudoVar)
|
||||
}
|
||||
|
||||
return nil, errors.New("unknown pseudo-variable: " + *n.Var)
|
||||
case n.MaybeSub != nil:
|
||||
sub := n.MaybeSub.Sub
|
||||
if sub == nil {
|
||||
|
@ -357,3 +367,8 @@ func (e evaluator) evalSub(ctx context.Context, ec *evalCtx, n *astPipeline) (Ob
|
|||
}
|
||||
return pipelineRes, nil
|
||||
}
|
||||
|
||||
type pseudoVar interface {
|
||||
get(ctx context.Context, name string) (Object, error)
|
||||
set(ctx context.Context, name string, v Object) error
|
||||
}
|
||||
|
|
88
ucl/inst.go
88
ucl/inst.go
|
@ -9,9 +9,10 @@ import (
|
|||
)
|
||||
|
||||
type Inst struct {
|
||||
out io.Writer
|
||||
missingBuiltinHandler MissingBuiltinHandler
|
||||
echoPrinter EchoPrinter
|
||||
out io.Writer
|
||||
missingBuiltinHandler MissingBuiltinHandler
|
||||
missingPseudoVarHandler pseudoVar
|
||||
echoPrinter EchoPrinter
|
||||
|
||||
rootEC *evalCtx
|
||||
}
|
||||
|
@ -121,6 +122,17 @@ func (inst *Inst) SetVar(name string, value any) {
|
|||
inst.rootEC.setOrDefineVar(name, obj)
|
||||
}
|
||||
|
||||
func (inst *Inst) SetPseudoVar(name string, h PseudoVarHandler) {
|
||||
if inst.rootEC.pseudoVars == nil {
|
||||
inst.rootEC.pseudoVars = make(map[string]pseudoVar)
|
||||
}
|
||||
inst.rootEC.pseudoVars[name] = nativePseudoVarHandler{h: h}
|
||||
}
|
||||
|
||||
func (inst *Inst) SetMissingPseudoVarHandler(h MissingPseudoVarHandler) {
|
||||
inst.missingPseudoVarHandler = nativeMissingPseudoVarHandler{h: h}
|
||||
}
|
||||
|
||||
func (inst *Inst) Out() io.Writer {
|
||||
if inst.out == nil {
|
||||
return os.Stdout
|
||||
|
@ -156,3 +168,73 @@ func (inst *Inst) eval(ctx context.Context, expr string) (Object, error) {
|
|||
// TODO: this should be a separate forkAndIsolate() session
|
||||
return eval.evalScript(ctx, inst.rootEC, ast)
|
||||
}
|
||||
|
||||
type PseudoVarHandler interface {
|
||||
Get(ctx context.Context) (any, error)
|
||||
}
|
||||
|
||||
type ModifiablePseudoVarHandler interface {
|
||||
PseudoVarHandler
|
||||
Set(ctx context.Context, v any) error
|
||||
}
|
||||
|
||||
type MissingPseudoVarHandler interface {
|
||||
Get(ctx context.Context, name string) (any, error)
|
||||
}
|
||||
|
||||
type MissingModifiablePseudoVarHandler interface {
|
||||
MissingPseudoVarHandler
|
||||
Set(ctx context.Context, string, v any) error
|
||||
}
|
||||
|
||||
type nativePseudoVarHandler struct {
|
||||
h PseudoVarHandler
|
||||
}
|
||||
|
||||
func (n nativePseudoVarHandler) get(ctx context.Context, name string) (Object, error) {
|
||||
gv, err := n.h.Get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fromGoValue(gv)
|
||||
}
|
||||
|
||||
func (n nativePseudoVarHandler) set(ctx context.Context, name string, v Object) error {
|
||||
mpvh, ok := n.h.(ModifiablePseudoVarHandler)
|
||||
if !ok {
|
||||
return errors.New("cannot set read-only pseudo-var")
|
||||
}
|
||||
|
||||
gv, ok := toGoValue(v)
|
||||
if !ok {
|
||||
return errors.New("cannot set non-matching type")
|
||||
}
|
||||
|
||||
return mpvh.Set(ctx, gv)
|
||||
}
|
||||
|
||||
type nativeMissingPseudoVarHandler struct {
|
||||
h MissingPseudoVarHandler
|
||||
}
|
||||
|
||||
func (n nativeMissingPseudoVarHandler) get(ctx context.Context, name string) (Object, error) {
|
||||
gv, err := n.h.Get(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fromGoValue(gv)
|
||||
}
|
||||
|
||||
func (n nativeMissingPseudoVarHandler) set(ctx context.Context, name string, v Object) error {
|
||||
mpvh, ok := n.h.(MissingModifiablePseudoVarHandler)
|
||||
if !ok {
|
||||
return errors.New("cannot set read-only pseudo-var")
|
||||
}
|
||||
|
||||
gv, ok := toGoValue(v)
|
||||
if !ok {
|
||||
return errors.New("cannot set non-matching type")
|
||||
}
|
||||
|
||||
return mpvh.Set(ctx, name, gv)
|
||||
}
|
||||
|
|
|
@ -124,6 +124,49 @@ func TestInst_Eval(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestInst_SetPseudoVar(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
expr string
|
||||
wantRes any
|
||||
wantBarVar string
|
||||
wantErr bool
|
||||
}{
|
||||
{desc: "read var 1", expr: `@foo`, wantRes: "this is foo"},
|
||||
{desc: "read var 2", expr: `@bar`, wantRes: "this is bar"},
|
||||
{desc: "read var 3", expr: `@fla`, wantRes: "missing value of fla"},
|
||||
|
||||
{desc: "write var 1", expr: `set "@foo" "hello" ; @foo`, wantRes: "hello"},
|
||||
{desc: "write var 2", expr: `set "@bar" "world" ; @bar`, wantRes: "world", wantBarVar: "world"},
|
||||
{desc: "write var 3", expr: `set "@blong" "hello"`, wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
bar := &stringPseudoVar{str: "this is bar"}
|
||||
|
||||
inst := ucl.New(ucl.WithTestBuiltin())
|
||||
inst.SetPseudoVar("foo", &stringPseudoVar{str: "this is foo"})
|
||||
inst.SetPseudoVar("bar", bar)
|
||||
inst.SetMissingPseudoVarHandler(missingPseudoVarType{})
|
||||
|
||||
res, err := inst.Eval(t.Context(), tt.expr)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantRes, res)
|
||||
|
||||
if tt.wantBarVar != "" {
|
||||
assert.Equal(t, tt.wantBarVar, bar.str)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var parseComments1 = `
|
||||
proc lookup { |file|
|
||||
foreach { |toks|
|
||||
|
@ -162,3 +205,22 @@ var parseComments4 = `
|
|||
}
|
||||
}
|
||||
# this use to fail`
|
||||
|
||||
type stringPseudoVar struct {
|
||||
str string
|
||||
}
|
||||
|
||||
func (s *stringPseudoVar) Get(ctx context.Context) (any, error) {
|
||||
return s.str, nil
|
||||
}
|
||||
|
||||
func (s *stringPseudoVar) Set(ctx context.Context, v any) error {
|
||||
s.str = v.(string)
|
||||
return nil
|
||||
}
|
||||
|
||||
type missingPseudoVarType struct{}
|
||||
|
||||
func (missingPseudoVarType) Get(ctx context.Context, name string) (any, error) {
|
||||
return "missing value of " + name, nil
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue