From e1b53f8bea4b2d614fd65c99ff6f0720b7d08b63 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Tue, 8 Sep 2026 20:36:39 +1000 Subject: [PATCH] Added os bang --- ucl/builtins/os.go | 36 ++++++++++++++++++++++++++++++++++++ ucl/builtins/os_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/ucl/builtins/os.go b/ucl/builtins/os.go index e19c19f..925f9c8 100644 --- a/ucl/builtins/os.go +++ b/ucl/builtins/os.go @@ -13,6 +13,7 @@ import ( type OSProvider interface { LookupEnv(string) (string, bool) Exec(ctx context.Context, cmd string, args ...string) (*exec.Cmd, error) + ExecBang(ctx context.Context, cmd string) (*exec.Cmd, error) } type osHandlers struct { @@ -29,6 +30,7 @@ func OS() ucl.Module { Builtins: map[string]ucl.BuiltinHandler{ "env": osh.env, "exec": osh.exec, + "!": osh.bang, }, } } @@ -89,8 +91,42 @@ func (oh osHandlers) exec(ctx context.Context, args ucl.CallArgs) (any, error) { return string(res), nil } +func (oh osHandlers) bang(ctx context.Context, args ucl.CallArgs) (any, error) { + var ( + cmdStr string + ) + + if err := args.Bind(&cmdStr); err != nil { + return nil, err + } + + cmd, err := oh.provider.ExecBang(ctx, cmdStr) + if err != nil { + return nil, err + } + + if args.NArgs() > 0 { + var stdin string + if err := args.Bind(&stdin); err != nil { + return nil, err + } + cmd.Stdin = strings.NewReader(stdin) + } + + res, err := cmd.Output() + if err != nil { + return nil, err + } + + return string(res), nil +} + type builtinOSProvider struct{} +func (p builtinOSProvider) ExecBang(ctx context.Context, cmd string) (*exec.Cmd, error) { + return exec.CommandContext(ctx, "bash", "-c", cmd), nil +} + func (builtinOSProvider) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) } diff --git a/ucl/builtins/os_test.go b/ucl/builtins/os_test.go index 78cee7f..748d632 100644 --- a/ucl/builtins/os_test.go +++ b/ucl/builtins/os_test.go @@ -60,3 +60,27 @@ func TestOS_Exec(t *testing.T) { }) } } + +func TestOS_Bang(t *testing.T) { + tests := []struct { + descr string + eval string + want any + }{ + {descr: "run command 1", eval: `os:! "echo 'hello, world'"`, want: "hello, world\n"}, + {descr: "run command 2", eval: `os:! "date +%Y%m%d"`, want: time.Now().Format("20060102") + "\n"}, + {descr: "run command 3", eval: `os:! "tr [a-z] [A-Z]" "hello"`, want: "HELLO"}, + {descr: "run command 4", eval: `os:! "tr -d e" "hello"`, want: "hllo"}, + } + + 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) + }) + } +}