Added os:exec builtin #2

Merged
lmika merged 1 commit from feature/os-exec into main 2025-10-24 06:09:45 +00:00
4 changed files with 81 additions and 5 deletions

2
go.mod
View file

@ -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
)

1
go.sum
View file

@ -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=

View file

@ -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,
"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
}

View file

@ -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)
})
}
}