Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions pkg/executor/dry.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,7 @@ func (ex *Dry) Run(_ context.Context, cmd string, _ *RunOpts) (out []string, err
var stdoutBuf bytes.Buffer
mwr := io.MultiWriter(ex.logs.Out, &stdoutBuf)
mwr.Write([]byte(cmd)) // nolint
for line := range strings.SplitSeq(stdoutBuf.String(), "\n") {
if line != "" {
out = append(out, line)
}
}
return out, nil
return splitOutputLines(stdoutBuf.String()), nil
}

// Upload doesn't actually upload, just prints the command
Expand Down
17 changes: 13 additions & 4 deletions pkg/executor/dry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,19 @@ import (
func TestDry_Run(t *testing.T) {
ctx := context.Background()
dry := NewDry(MakeLogs(true, false, nil))
res, err := dry.Run(ctx, "ls -la /srv", &RunOpts{Verbose: true})
require.NoError(t, err)
require.Len(t, res, 1)
require.Equal(t, "ls -la /srv", res[0])

t.Run("single line", func(t *testing.T) {
res, err := dry.Run(ctx, "ls -la /srv", &RunOpts{Verbose: true})
require.NoError(t, err)
require.Len(t, res, 1)
require.Equal(t, "ls -la /srv", res[0])
})

t.Run("multi line with blank line", func(t *testing.T) {
res, err := dry.Run(ctx, "ls -la /srv\n\ndf -h\n", &RunOpts{Verbose: true})
require.NoError(t, err)
require.Equal(t, []string{"ls -la /srv", "", "df -h"}, res)
})
}

func TestDryUpload(t *testing.T) {
Expand Down
16 changes: 16 additions & 0 deletions pkg/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ type DeleteOpts struct {
// exclude patterns and remote paths always use forward slashes.
func normalizeSlashes(s string) string { return strings.ReplaceAll(s, `\`, "/") }

// splitOutputLines splits captured command output into lines the same way bufio.ScanLines does, i.e. dropping
// a trailing \r and the empty element after the final newline, but without the scanner's token size limit.
// All executors share it, so the same stdout produces the same lines regardless of the implementation.
// The output is already fully buffered, so a single line of any length is returned as is.
func splitOutputLines(s string) []string {
if s == "" {
return nil
}
lines := strings.Split(strings.TrimSuffix(s, "\n"), "\n")
res := make([]string, 0, len(lines))
for _, line := range lines {
res = append(res, strings.TrimSuffix(line, "\r"))
}
return res
}

// isExcluded reports whether fpath matches any of the exclude patterns. A pattern ending in "/*"
// also matches the directory it names, so the whole subtree is protected, but this directory match
// only applies when fpath is itself a directory. This prevents a pattern like "dir*/*" from
Expand Down
22 changes: 22 additions & 0 deletions pkg/executor/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,25 @@ func captureStdOut(t *testing.T, f func()) string {
io.Copy(&buf, r)
return buf.String()
}

func TestSplitOutputLines(t *testing.T) {
tbl := []struct {
name string
in string
res []string
}{
{"empty", "", nil},
{"single line, no trailing newline", "hello", []string{"hello"}},
{"single line with trailing newline", "hello\n", []string{"hello"}},
{"multiple lines", "line1\nline2\nline3\n", []string{"line1", "line2", "line3"}},
{"blank line in the middle", "line1\n\nline2\n", []string{"line1", "", "line2"}},
{"single newline", "\n", []string{""}},
{"trailing blank line", "line1\n\n", []string{"line1", ""}},
{"crlf line endings", "line1\r\nline2\r\n", []string{"line1", "line2"}},
}
for _, tt := range tbl {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.res, splitOutputLines(tt.in))
})
}
}
15 changes: 0 additions & 15 deletions pkg/executor/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,6 @@ func (l *Local) Run(ctx context.Context, cmd string, _ *RunOpts) (out []string,
return splitOutputLines(stdoutBuf.String()), nil
}

// splitOutputLines splits captured command output into lines the same way bufio.ScanLines does, i.e. dropping
// a trailing \r and the empty element after the final newline, but without the scanner's token size limit.
// The output is already fully buffered, so a single line of any length is returned as is.
func splitOutputLines(s string) []string {
if s == "" {
return nil
}
lines := strings.Split(strings.TrimSuffix(s, "\n"), "\n")
res := make([]string, 0, len(lines))
for _, line := range lines {
res = append(res, strings.TrimSuffix(line, "\r"))
}
return res
}

// Upload just copy file from one place to another
func (l *Local) Upload(ctx context.Context, src, dst string, opts *UpDownOpts) (err error) {

Expand Down
22 changes: 0 additions & 22 deletions pkg/executor/local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,28 +98,6 @@ func TestRun(t *testing.T) {
})
}

func TestSplitOutputLines(t *testing.T) {
tbl := []struct {
name string
in string
res []string
}{
{"empty", "", nil},
{"single line, no trailing newline", "hello", []string{"hello"}},
{"single line with trailing newline", "hello\n", []string{"hello"}},
{"multiple lines", "line1\nline2\nline3\n", []string{"line1", "line2", "line3"}},
{"blank line in the middle", "line1\n\nline2\n", []string{"line1", "", "line2"}},
{"single newline", "\n", []string{""}},
{"trailing blank line", "line1\n\n", []string{"line1", ""}},
{"crlf line endings", "line1\r\nline2\r\n", []string{"line1", "line2"}},
}
for _, tt := range tbl {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.res, splitOutputLines(tt.in))
})
}
}

func TestUploadAndDownload(t *testing.T) {
testCases := []struct {
name string
Expand Down
8 changes: 1 addition & 7 deletions pkg/executor/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"os"
"path/filepath"
"slices"
"strings"
"time"

"github.com/pkg/sftp"
Expand Down Expand Up @@ -357,12 +356,7 @@ func (ex *Remote) sshRun(ctx context.Context, client *ssh.Client, command string
return nil, fmt.Errorf("canceled: %w", ctx.Err())
}

for line := range strings.SplitSeq(stdoutBuf.String(), "\n") {
if line != "" {
out = append(out, line)
}
}
return out, nil
return splitOutputLines(stdoutBuf.String()), nil
}

type sftpReq struct {
Expand Down
6 changes: 6 additions & 0 deletions pkg/executor/remote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,12 @@ func TestExecuter_Run(t *testing.T) {
assert.Equal(t, "data2.txt", out[1])
})

t.Run("blank lines and crlf preserved as local does", func(t *testing.T) {
out, e := sess.Run(ctx, "sh -c 'printf \"line1\\n\\nline2\\r\\n\"'", nil)
require.NoError(t, e)
assert.Equal(t, []string{"line1", "", "line2"}, out)
})

t.Run("find out", func(t *testing.T) {
cmd := fmt.Sprintf("find %s -type f -exec stat -c '%%n:%%s' {} \\;", "/tmp/")
out, e := sess.Run(ctx, cmd, &RunOpts{Verbose: true})
Expand Down
8 changes: 7 additions & 1 deletion pkg/runner/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,13 @@ func (ec *execCmd) Echo(ctx context.Context) (resp execCmdResp, err error) {
if err != nil {
return resp, ec.errorFmt("can't run echo command on %s: %w", ec.hostAddr, err)
}
resp.details = fmt.Sprintf(" {echo: %s}", strings.Join(out, "; "))
printed := make([]string, 0, len(out))
for _, line := range out { // empty lines carry nothing for the report and would show up as empty segments
if line != "" {
printed = append(printed, line)
}
}
resp.details = fmt.Sprintf(" {echo: %s}", strings.Join(printed, "; "))
return resp, nil
}

Expand Down
14 changes: 14 additions & 0 deletions pkg/runner/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,20 @@ func Test_execEcho(t *testing.T) {
assert.Equal(t, " {echo: foo welcome back}", resp.details)
})

t.Run("echo command with blank line in output", func(t *testing.T) {
ec := execCmd{exec: sess, tsk: &config.Task{Name: "test"}, cmd: config.Cmd{Echo: "$(echo first; echo; echo second)", Name: "test"}}
resp, err := ec.Echo(ctx)
require.NoError(t, err)
assert.Equal(t, " {echo: first; second}", resp.details)
})

t.Run("echo command with whitespace-only line in output", func(t *testing.T) {
ec := execCmd{exec: sess, tsk: &config.Task{Name: "test"}, cmd: config.Cmd{Echo: "$(echo first; echo ' '; echo second)", Name: "test"}}
resp, err := ec.Echo(ctx)
require.NoError(t, err)
assert.Equal(t, " {echo: first; ; second}", resp.details)
})

t.Run("echo command with condition true", func(t *testing.T) {
defer os.Remove("/tmp/test.condition")
_, err := sess.Run(ctx, "touch /tmp/test.condition", nil)
Expand Down