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.

feat(tee): accept -i/--ignore-interrupts as documented no-op

Implements GNU coreutils compatibility for tee:

- -i/--ignore-interrupts accepted as no-op since kefka
Execers run in-process and receive no SIGINT
- Combined short flags (-ai) honored
- Continue-on-error semantics with per-file stderr
diagnostics

Retains -a/--append.

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

Xe Iaso ed7a4ce1 65aa225d

+108 -4
+24 -4
command/internal/tee/tee.go
··· 37 37 usage := func() { 38 38 fmt.Fprint(stderr, "Usage: tee [OPTION]... [FILE]...\n") 39 39 fmt.Fprint(stderr, "Copy standard input to each FILE, and also to standard output.\n\n") 40 - fmt.Fprint(stderr, " -a, --append append to the given FILEs, do not overwrite\n") 41 - fmt.Fprint(stderr, " --help display this help and exit\n") 40 + fmt.Fprint(stderr, " -a, --append append to the given FILEs, do not overwrite\n") 41 + fmt.Fprint(stderr, " -i, --ignore-interrupts ignore interrupt signals\n") 42 + fmt.Fprint(stderr, " --help display this help and exit\n") 42 43 } 43 44 set.SetUsage(usage) 44 45 45 46 appendMode := set.BoolLong("append", 'a', "append to the given FILEs, do not overwrite") 47 + // -i is accepted for GNU coreutils 9.x compatibility but is a no-op: 48 + // kefka runs in-process under mvdan.cc/sh, so signal handling is the 49 + // host shell's responsibility, not ours. 50 + _ = set.BoolLong("ignore-interrupts", 'i', "ignore interrupt signals") 46 51 help := set.BoolLong("help", 0, "display this help and exit") 47 52 48 53 if err := set.Getopt(append([]string{"tee"}, args...), nil); err != nil { ··· 54 59 usage() 55 60 return nil 56 61 } 57 - 58 62 files := set.Args() 59 63 60 64 var content []byte ··· 67 71 content = data 68 72 } 69 73 74 + // GNU tee continues writing to remaining files (and to stdout) when one 75 + // file fails; it just exits non-zero at the end. 70 76 exitCode := 0 71 77 for _, file := range files { 72 78 if err := writeFile(ec, file, content, *appendMode); err != nil { 73 - fmt.Fprintf(stderr, "tee: %s: No such file or directory\n", file) 79 + fmt.Fprintf(stderr, "tee: %s: %s\n", file, errMessage(err)) 74 80 exitCode = 1 75 81 } 76 82 } ··· 84 90 return interp.ExitStatus(uint8(exitCode)) 85 91 } 86 92 return nil 93 + } 94 + 95 + // errMessage extracts a GNU-tee-style short error message from err. Billy 96 + // returns wrapped *PathError values for most failures; the underlying error's 97 + // Error() is what GNU coreutils prints (e.g. "Permission denied"). When we 98 + // can't pull a more specific cause out, we default to "No such file or 99 + // directory" — billy's memfs does not always wrap missing-parent errors as 100 + // *PathError, and this matches what users hit most often. 101 + func errMessage(err error) string { 102 + var pe *os.PathError 103 + if errors.As(err, &pe) && pe.Err != nil { 104 + return pe.Err.Error() 105 + } 106 + return "No such file or directory" 87 107 } 88 108 89 109 func writeFile(ec *command.ExecContext, file string, content []byte, appendMode bool) error {
+84
command/internal/tee/tee_test.go
··· 3 3 import ( 4 4 "bytes" 5 5 "context" 6 + "errors" 6 7 "io" 7 8 "os" 8 9 "strings" ··· 127 128 wantFiles: map[string]string{"bin.dat": "a\x00b\xffc"}, 128 129 }, 129 130 { 131 + name: "ignore-interrupts short flag is accepted as no-op", 132 + args: []string{"-i", "out.txt"}, 133 + stdin: "hello", 134 + wantStdout: "hello", 135 + wantFiles: map[string]string{"out.txt": "hello"}, 136 + }, 137 + { 138 + name: "ignore-interrupts long flag is accepted as no-op", 139 + args: []string{"--ignore-interrupts", "out.txt"}, 140 + stdin: "hello", 141 + wantStdout: "hello", 142 + wantFiles: map[string]string{"out.txt": "hello"}, 143 + }, 144 + { 145 + name: "append plus ignore-interrupts combined short flags", 146 + args: []string{"-ai", "existing.txt"}, 147 + stdin: "added\n", 148 + wantStdout: "added\n", 149 + wantFiles: map[string]string{"existing.txt": "old contents\nadded\n"}, 150 + }, 151 + { 130 152 name: "unknown flag errors", 131 153 args: []string{"--nope"}, 132 154 wantErr: true, ··· 177 199 } 178 200 if !strings.Contains(stderr.String(), "tee: out.txt: No such file or directory") { 179 201 t.Errorf("stderr missing expected message: %q", stderr.String()) 202 + } 203 + } 204 + 205 + // failingFS wraps a billy.Filesystem and forces OpenFile to fail with errFail 206 + // for any path equal to failPath. Every other call delegates to the inner FS. 207 + type failingFS struct { 208 + billy.Filesystem 209 + failPath string 210 + errFail error 211 + } 212 + 213 + func (f *failingFS) OpenFile(name string, flag int, perm os.FileMode) (billy.File, error) { 214 + if name == f.failPath { 215 + return nil, &os.PathError{Op: "open", Path: name, Err: f.errFail} 216 + } 217 + return f.Filesystem.OpenFile(name, flag, perm) 218 + } 219 + 220 + func (f *failingFS) Create(name string) (billy.File, error) { 221 + if name == f.failPath { 222 + return nil, &os.PathError{Op: "create", Path: name, Err: f.errFail} 223 + } 224 + return f.Filesystem.Create(name) 225 + } 226 + 227 + func TestFailureOnOneFileKeepsWritingOthers(t *testing.T) { 228 + inner := memfs.New() 229 + fs := &failingFS{ 230 + Filesystem: inner, 231 + failPath: "bad.txt", 232 + errFail: errors.New("Permission denied"), 233 + } 234 + 235 + var stdout, stderr bytes.Buffer 236 + ec := &command.ExecContext{ 237 + Stdin: strings.NewReader("payload\n"), 238 + Stdout: &stdout, 239 + Stderr: &stderr, 240 + Dir: ".", 241 + FS: fs, 242 + } 243 + 244 + err := Impl{}.Exec(context.Background(), ec, []string{"good1.txt", "bad.txt", "good2.txt"}) 245 + if err == nil { 246 + t.Fatalf("expected non-nil error (exit status), got nil; stderr=%q", stderr.String()) 247 + } 248 + 249 + if stdout.String() != "payload\n" { 250 + t.Errorf("stdout = %q, want %q", stdout.String(), "payload\n") 251 + } 252 + if got := readFile(t, inner, "good1.txt"); got != "payload\n" { 253 + t.Errorf("good1.txt = %q, want %q (failure on bad.txt must not abort prior writes)", got, "payload\n") 254 + } 255 + if got := readFile(t, inner, "good2.txt"); got != "payload\n" { 256 + t.Errorf("good2.txt = %q, want %q (failure on bad.txt must not stop later writes)", got, "payload\n") 257 + } 258 + if !strings.Contains(stderr.String(), "tee: bad.txt: Permission denied") { 259 + t.Errorf("stderr missing per-file failure message: %q", stderr.String()) 260 + } 261 + // The good files must not show up in stderr. 262 + if strings.Contains(stderr.String(), "good1.txt") || strings.Contains(stderr.String(), "good2.txt") { 263 + t.Errorf("stderr should only mention the failing file: %q", stderr.String()) 180 264 } 181 265 } 182 266