diff --git a/go.mod b/go.mod index b65ae1b..b7b4cb6 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/chzyer/readline v1.5.1 github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f github.com/stretchr/testify v1.10.0 + lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b ) require ( @@ -17,5 +18,4 @@ require ( go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b // indirect ) diff --git a/go.sum b/go.sum index a9528c9..99a7730 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= diff --git a/ucl/builtins/os.go b/ucl/builtins/os.go index e51f62f..9274bc7 100644 --- a/ucl/builtins/os.go +++ b/ucl/builtins/os.go @@ -2,20 +2,32 @@ package builtins import ( "context" + "errors" "os" + "os/exec" + "ucl.lmika.dev/ucl" ) +type OSProvider interface { + LookupEnv(string) (string, bool) + Exec(ctx context.Context, cmd string, args ...string) (*exec.Cmd, error) +} + type osHandlers struct { + provider OSProvider } func OS() ucl.Module { - osh := osHandlers{} + osh := osHandlers{ + provider: builtinOSProvider{}, + } return ucl.Module{ Name: "os", Builtins: map[string]ucl.BuiltinHandler{ - "env": osh.env, + "env": osh.env, + "exec": osh.exec, }, } } @@ -26,7 +38,7 @@ func (oh osHandlers) env(ctx context.Context, args ucl.CallArgs) (any, error) { return nil, err } - val, ok := os.LookupEnv(envName) + val, ok := oh.provider.LookupEnv(envName) if ok { return val, nil } @@ -38,3 +50,42 @@ func (oh osHandlers) env(ctx context.Context, args ucl.CallArgs) (any, error) { return "", nil } + +func (oh osHandlers) exec(ctx context.Context, args ucl.CallArgs) (any, error) { + var cmdArgs []string + + for args.NArgs() > 0 { + var s string + if err := args.Bind(&s); err != nil { + return nil, err + } + + cmdArgs = append(cmdArgs, s) + } + + if len(cmdArgs) == 0 { + return nil, errors.New("expected command") + } + + cmd, err := oh.provider.Exec(ctx, cmdArgs[0], cmdArgs[1:]...) + if err != nil { + return nil, err + } + + res, err := cmd.Output() + if err != nil { + return nil, err + } + + return string(res), nil +} + +type builtinOSProvider struct{} + +func (builtinOSProvider) LookupEnv(key string) (string, bool) { + return os.LookupEnv(key) +} + +func (builtinOSProvider) Exec(ctx context.Context, name string, args ...string) (*exec.Cmd, error) { + return exec.CommandContext(ctx, name, args...), nil +} diff --git a/ucl/builtins/os_test.go b/ucl/builtins/os_test.go index cbefcbf..73dc1cb 100644 --- a/ucl/builtins/os_test.go +++ b/ucl/builtins/os_test.go @@ -2,8 +2,10 @@ package builtins_test import ( "context" - "github.com/stretchr/testify/assert" "testing" + "time" + + "github.com/stretchr/testify/assert" "ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl/builtins" ) @@ -34,3 +36,25 @@ func TestOS_Env(t *testing.T) { }) } } + +func TestOS_Exec(t *testing.T) { + tests := []struct { + descr string + eval string + want any + }{ + {descr: "run command 1", eval: `os:exec "echo" "hello, world"`, want: "hello, world\n"}, + {descr: "run command 1", eval: `os:exec "date" "+%Y%m%d"`, want: time.Now().Format("20060102") + "\n"}, + } + + for _, tt := range tests { + t.Run(tt.descr, func(t *testing.T) { + inst := ucl.New( + ucl.WithModule(builtins.OS()), + ) + res, err := inst.EvalString(context.Background(), tt.eval) + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + }) + } +}