A virtual jailed shell environment for Go apps backed by an io/fs#FS.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(time): print usage and exit 1 when no command given

Aligns with GNU /usr/bin/time: invoking time with no
command now prints usage to stderr and exits non-zero,
rather than silently succeeding.

Adds tests covering exit-status propagation (time true /
time false), the -p portable format three-line output,
and -f '%e' elapsed time.

%U/%S/%P/%M remain static placeholders since kefka has no
rusage source for in-process Execers.

Refs: docs/posix2018/CONFORMANCE.md
Assisted-by: Claude Opus 4.7 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>

Xe Iaso ee2a39db ed7a4ce1

+85 -6
+4 -2
command/internal/time/time.go
··· 111 111 112 112 commandArgs := args[i:] 113 113 114 - // No command specified — return success silently (matches GNU time). 114 + // No command specified — print usage and exit non-zero, matching 115 + // GNU /usr/bin/time which errors out with "Usage: ..." in this case. 115 116 if len(commandArgs) == 0 { 116 - return nil 117 + usage() 118 + return interp.ExitStatus(1) 117 119 } 118 120 119 121 displayCommand := strings.Join(commandArgs, " ")
+81 -4
command/internal/time/time_test.go
··· 36 36 return interp.ExitStatus(3) 37 37 } 38 38 39 + // trueImpl always succeeds (mirrors GNU `true`). 40 + type trueImpl struct{} 41 + 42 + func (trueImpl) Exec(ctx context.Context, ec *command.ExecContext, args []string) error { 43 + return nil 44 + } 45 + 46 + // falseImpl always exits with status 1 (mirrors GNU `false`). 47 + type falseImpl struct{} 48 + 49 + func (falseImpl) Exec(ctx context.Context, ec *command.ExecContext, args []string) error { 50 + return interp.ExitStatus(1) 51 + } 52 + 39 53 // stdinEchoImpl reads from stdin and writes it to stdout, used to verify 40 54 // time passes the outer stdin through to the inner command. 41 55 type stdinEchoImpl struct{} ··· 58 72 reg.Register("echo", echoImpl{}) 59 73 reg.Register("fail", failImpl{}) 60 74 reg.Register("stdin-echo", stdinEchoImpl{}) 75 + reg.Register("true", trueImpl{}) 76 + reg.Register("false", falseImpl{}) 61 77 return reg 62 78 } 63 79 ··· 95 111 96 112 func TestTime_NoCommand(t *testing.T) { 97 113 r := run(t, nil) 114 + var status interp.ExitStatus 115 + if !errors.As(r.err, &status) || uint8(status) != 1 { 116 + t.Errorf("err = %v, want ExitStatus(1)", r.err) 117 + } 118 + if r.stdout != "" { 119 + t.Errorf("stdout = %q, want empty", r.stdout) 120 + } 121 + if !strings.Contains(r.stderr, "Usage: time") { 122 + t.Errorf("stderr missing usage; got: %q", r.stderr) 123 + } 124 + } 125 + 126 + func TestTime_True(t *testing.T) { 127 + // `time true` succeeds (exit 0) and writes timing info to stderr. 128 + r := run(t, []string{"true"}) 98 129 if r.err != nil { 99 130 t.Fatalf("unexpected error: %v", r.err) 100 131 } 101 - if r.stdout != "" { 102 - t.Errorf("stdout = %q, want empty", r.stdout) 132 + if r.stderr == "" { 133 + t.Errorf("expected timing on stderr, got empty") 134 + } 135 + } 136 + 137 + func TestTime_False(t *testing.T) { 138 + // `time false` propagates the inner exit status of 1. 139 + r := run(t, []string{"false"}) 140 + var status interp.ExitStatus 141 + if !errors.As(r.err, &status) || uint8(status) != 1 { 142 + t.Errorf("err = %v, want ExitStatus(1)", r.err) 143 + } 144 + if r.stderr == "" { 145 + t.Errorf("expected timing on stderr even when inner command fails") 146 + } 147 + } 148 + 149 + func TestTime_PortableFormat_True(t *testing.T) { 150 + // `time -p true` emits the three-line POSIX portable format on stderr. 151 + r := run(t, []string{"-p", "true"}) 152 + if r.err != nil { 153 + t.Fatalf("unexpected error: %v", r.err) 154 + } 155 + lines := strings.Split(strings.TrimRight(r.stderr, "\n"), "\n") 156 + if len(lines) != 3 { 157 + t.Fatalf("expected 3 lines, got %d: %q", len(lines), r.stderr) 103 158 } 104 - if r.stderr != "" { 105 - t.Errorf("stderr = %q, want empty", r.stderr) 159 + if !strings.HasPrefix(lines[0], "real ") { 160 + t.Errorf("line 0 = %q, want prefix %q", lines[0], "real ") 161 + } 162 + if !strings.HasPrefix(lines[1], "user ") { 163 + t.Errorf("line 1 = %q, want prefix %q", lines[1], "user ") 164 + } 165 + if !strings.HasPrefix(lines[2], "sys ") { 166 + t.Errorf("line 2 = %q, want prefix %q", lines[2], "sys ") 167 + } 168 + } 169 + 170 + func TestTime_FormatElapsedFlag(t *testing.T) { 171 + // `time -f '%e' true` writes only the elapsed-time substitution. 172 + r := run(t, []string{"-f", "%e", "true"}) 173 + if r.err != nil { 174 + t.Fatalf("unexpected error: %v", r.err) 175 + } 176 + got := strings.TrimRight(r.stderr, "\n") 177 + // %e expands to "%.2f" — verify a simple decimal pattern. 178 + if got == "" { 179 + t.Fatalf("stderr empty, want elapsed time") 180 + } 181 + if !strings.Contains(got, ".") { 182 + t.Errorf("stderr = %q, want decimal elapsed time", got) 106 183 } 107 184 } 108 185