From b75ba32cc27672c3f57fed81b10399c8fd6dde42 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 3 Apr 2026 12:58:03 +0000 Subject: [PATCH] Add shell executable support --- modules.go | 19 +++++++++++++++++-- modules_cmd_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/modules.go b/modules.go index 5ef4bb0..c76300b 100644 --- a/modules.go +++ b/modules.go @@ -318,7 +318,7 @@ func (e *Executor) moduleShell(ctx context.Context, client sshExecutorClient, ar cmd = prefixCommandStdin(cmd, stdin, getBoolArg(args, "stdin_add_newline", true)) } - stdout, stderr, rc, err := client.RunScript(ctx, cmd) + stdout, stderr, rc, err := runShellScriptCommand(ctx, client, cmd, getStringArg(args, "executable", "")) if err != nil { return &TaskResult{Failed: true, Msg: err.Error(), Stdout: stdout, Stderr: stderr, RC: rc}, nil } @@ -508,7 +508,7 @@ func (e *Executor) moduleScript(ctx context.Context, client sshExecutorClient, a data = sprintf("cd %q && %s", chdir, data) } - stdout, stderr, rc, err := client.RunScript(ctx, data) + stdout, stderr, rc, err := runShellScriptCommand(ctx, client, data, getStringArg(args, "executable", "")) if err != nil { return &TaskResult{Failed: true, Msg: err.Error()}, nil } @@ -522,6 +522,21 @@ func (e *Executor) moduleScript(ctx context.Context, client sshExecutorClient, a }, nil } +// runShellScriptCommand executes a shell script using either the default +// heredoc path or a caller-specified executable. +// +// Example: +// +// stdout, stderr, rc, err := runShellScriptCommand(ctx, client, "echo hi", "/bin/dash") +func runShellScriptCommand(ctx context.Context, client sshExecutorClient, script, executable string) (stdout, stderr string, exitCode int, err error) { + if executable == "" { + return client.RunScript(ctx, script) + } + + cmd := sprintf("%s -c %s", shellSingleQuote(executable), shellSingleQuote(script)) + return client.Run(ctx, cmd) +} + // --- File Modules --- func (e *Executor) moduleCopy(ctx context.Context, client sshExecutorClient, args map[string]any, host string, task *Task) (*TaskResult, error) { diff --git a/modules_cmd_test.go b/modules_cmd_test.go index 0768716..5ac92cc 100644 --- a/modules_cmd_test.go +++ b/modules_cmd_test.go @@ -415,6 +415,26 @@ func TestModulesCmd_ModuleShell_Good_WithChdir(t *testing.T) { assert.Contains(t, last.Cmd, "npm install") } +func TestModulesCmd_ModuleShell_Good_ExecutableUsesRun(t *testing.T) { + e, mock := newTestExecutorWithMock("host1") + mock.expectCommand(`/bin/dash.*echo test`, "test\n", "", 0) + + result, err := e.moduleShell(context.Background(), mock, map[string]any{ + "_raw_params": "echo test", + "executable": "/bin/dash", + }) + + require.NoError(t, err) + assert.True(t, result.Changed) + + last := mock.lastCommand() + require.NotNil(t, last) + assert.Equal(t, "Run", last.Method) + assert.Contains(t, last.Cmd, "/bin/dash") + assert.Contains(t, last.Cmd, "-c") + assert.Contains(t, last.Cmd, "echo test") +} + func TestModulesCmd_ModuleShell_Bad_NoCommand(t *testing.T) { e, _ := newTestExecutorWithMock("host1") mock := NewMockSSHClient() @@ -642,6 +662,31 @@ func TestModulesCmd_ModuleScript_Good_ChdirPrefixesScript(t *testing.T) { assert.Equal(t, `cd "/opt/app" && pwd`, last.Cmd) } +func TestModulesCmd_ModuleScript_Good_ExecutableUsesRun(t *testing.T) { + tmpDir := t.TempDir() + scriptPath := joinPath(tmpDir, "dash.sh") + require.NoError(t, writeTestFile(scriptPath, []byte("echo script works"), 0755)) + + e, mock := newTestExecutorWithMock("host1") + mock.expectCommand(`/bin/dash.*echo script works`, "script works\n", "", 0) + + result, err := e.moduleScript(context.Background(), mock, map[string]any{ + "_raw_params": scriptPath, + "executable": "/bin/dash", + }) + + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.Changed) + + last := mock.lastCommand() + require.NotNil(t, last) + assert.Equal(t, "Run", last.Method) + assert.Contains(t, last.Cmd, "/bin/dash") + assert.Contains(t, last.Cmd, "-c") + assert.Contains(t, last.Cmd, "echo script works") +} + func TestModulesCmd_ModuleScript_Bad_NoScript(t *testing.T) { e, _ := newTestExecutorWithMock("host1") mock := NewMockSSHClient()