Skip to content

Commit 5755e5e

Browse files
acmoreclaude
andauthored
fix(jobs): flatten multi-line commands in jobs list table (#154)
Detached commands are often multi-line `bash -c` strings with backslash continuations; their embedded newlines broke the one-row-per-job table layout regardless of the width cap. Join line-continuations and collapse whitespace runs to a single space before truncating. JSON output keeps the raw command. Also replaces example/test namespace and PVC placeholders with generic names. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 540461a commit 5755e5e

2 files changed

Lines changed: 54 additions & 1 deletion

File tree

internal/cli/exec_jobs.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func runExecJobs(ctx context.Context, client connect.ExecClient, namespace strin
8585
job.SummaryState,
8686
fmt.Sprintf("%d", job.Pods),
8787
job.StartedAt,
88-
truncateTableCell(job.Command, commandLimit),
88+
truncateTableCell(flattenCommandForTable(job.Command), commandLimit),
8989
})
9090
}
9191
output.PrintTable(out, []string{"JOB ID", "STATE", "PODS", "STARTED", "COMMAND"}, table)
@@ -366,6 +366,17 @@ func jobsListCommandLimitForWidth(termWidth int, jobs []logicalExecJobView) int
366366
return limit
367367
}
368368

369+
// flattenCommandForTable renders a possibly multi-line command as a single
370+
// table row: shell line-continuations (`\` + newline) are joined and all
371+
// remaining whitespace runs collapse to single spaces. Embedded newlines
372+
// would otherwise break the one-row-per-job layout no matter how the length
373+
// is capped. JSON output keeps the raw command.
374+
func flattenCommandForTable(s string) string {
375+
s = strings.ReplaceAll(s, "\\\r\n", " ")
376+
s = strings.ReplaceAll(s, "\\\n", " ")
377+
return strings.Join(strings.Fields(s), " ")
378+
}
379+
369380
func truncateTableCell(s string, limit int) string {
370381
if limit <= 0 {
371382
return s

internal/cli/exec_jobs_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,48 @@ func TestJobsListCommandLimitForWidth(t *testing.T) {
433433
}
434434
}
435435

436+
func TestFlattenCommandForTable(t *testing.T) {
437+
// Real-world detach commands are multi-line `bash -c` strings with
438+
// backslash continuations; the table must render them as one row.
439+
in := "bash -c 'cd /opt/train/recipe/pretrain/script && \\\nPYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \\\nMODEL_PATH=/data/models/base-model \\\n\tpython pretrain.py'"
440+
got := flattenCommandForTable(in)
441+
if strings.ContainsAny(got, "\n\r\t") {
442+
t.Fatalf("expected flattened single-line command, got %q", got)
443+
}
444+
want := "bash -c 'cd /opt/train/recipe/pretrain/script && PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True MODEL_PATH=/data/models/base-model python pretrain.py'"
445+
if got != want {
446+
t.Fatalf("unexpected flatten result:\n got %q\nwant %q", got, want)
447+
}
448+
// Newlines without continuation backslashes also collapse.
449+
if got := flattenCommandForTable("echo a\necho b"); got != "echo a echo b" {
450+
t.Fatalf("unexpected: %q", got)
451+
}
452+
if got := flattenCommandForTable("plain command"); got != "plain command" {
453+
t.Fatalf("single-line commands must pass through, got %q", got)
454+
}
455+
}
456+
457+
func TestRunExecJobsTableRendersMultilineCommandAsOneRow(t *testing.T) {
458+
meta := "{\"jobId\":\"job-a\",\"pod\":\"okdev-sess-worker-0\",\"container\":\"dev\",\"pid\":100," +
459+
"\"command\":\"bash -c 'cd /x && \\\\\\nENV=1 \\\\\\npython t.py'\"," +
460+
"\"startedAt\":\"2026-07-05T00:06:22Z\",\"stdoutPath\":\"/var/okdev/exec/job-a.log\",\"stderrPath\":\"/var/okdev/exec/job-a.log\",\"metaPath\":\"/var/okdev/exec/job-a.json\",\"state\":\"running\"}\n"
461+
client := &fakePdshExecClient{outputs: map[string]string{"okdev-sess-worker-0": meta}}
462+
pods := []kube.PodSummary{{Name: "okdev-sess-worker-0", Phase: "Running"}}
463+
var out bytes.Buffer
464+
if err := runExecJobs(context.Background(), client, "default", pods, "dev", "", pdshDefaultFanout, &out, false, fanoutRoute{}); err != nil {
465+
t.Fatalf("runExecJobs: %v", err)
466+
}
467+
got := out.String()
468+
if !strings.Contains(got, "bash -c 'cd /x && ENV=1 python t.py'") {
469+
t.Fatalf("expected flattened command in table, got %q", got)
470+
}
471+
for _, line := range strings.Split(got, "\n") {
472+
if strings.Contains(line, "job-a") && !strings.Contains(line, "python t.py'") {
473+
t.Fatalf("expected the job row to stay on one line, got %q", got)
474+
}
475+
}
476+
}
477+
436478
func TestTruncateTableCell(t *testing.T) {
437479
if got := truncateTableCell("short", 10); got != "short" {
438480
t.Fatalf("unexpected: %q", got)

0 commit comments

Comments
 (0)