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(command): port tee from just-bash

Assisted-by: Claude Opus 4.7 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>

Xe Iaso 8f5fe3a5 7b0d6700

+323
+126
command/internal/tee/tee.go
··· 1 + package tee 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "fmt" 7 + "io" 8 + "os" 9 + "path" 10 + "strings" 11 + 12 + "github.com/pborman/getopt/v2" 13 + "mvdan.cc/sh/v3/interp" 14 + "tangled.org/xeiaso.net/kefka/command" 15 + ) 16 + 17 + type Impl struct{} 18 + 19 + func (Impl) Exec(_ context.Context, ec *command.ExecContext, args []string) error { 20 + if ec == nil { 21 + return errors.New("tee: nil ExecContext") 22 + } 23 + 24 + stdout := ec.Stdout 25 + if stdout == nil { 26 + stdout = io.Discard 27 + } 28 + stderr := ec.Stderr 29 + if stderr == nil { 30 + stderr = io.Discard 31 + } 32 + 33 + set := getopt.New() 34 + set.SetProgram("tee") 35 + set.SetParameters("[FILE]...") 36 + 37 + usage := func() { 38 + fmt.Fprint(stderr, "Usage: tee [OPTION]... [FILE]...\n") 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") 42 + } 43 + set.SetUsage(usage) 44 + 45 + appendMode := set.BoolLong("append", 'a', "append to the given FILEs, do not overwrite") 46 + help := set.BoolLong("help", 0, "display this help and exit") 47 + 48 + if err := set.Getopt(append([]string{"tee"}, args...), nil); err != nil { 49 + fmt.Fprintf(stderr, "tee: %s\n", err) 50 + usage() 51 + return interp.ExitStatus(1) 52 + } 53 + if *help { 54 + usage() 55 + return nil 56 + } 57 + 58 + files := set.Args() 59 + 60 + var content []byte 61 + if ec.Stdin != nil { 62 + data, err := io.ReadAll(ec.Stdin) 63 + if err != nil { 64 + fmt.Fprintf(stderr, "tee: %v\n", err) 65 + return interp.ExitStatus(1) 66 + } 67 + content = data 68 + } 69 + 70 + exitCode := 0 71 + for _, file := range files { 72 + if err := writeFile(ec, file, content, *appendMode); err != nil { 73 + fmt.Fprintf(stderr, "tee: %s: No such file or directory\n", file) 74 + exitCode = 1 75 + } 76 + } 77 + 78 + if _, err := stdout.Write(content); err != nil { 79 + fmt.Fprintf(stderr, "tee: %v\n", err) 80 + return interp.ExitStatus(1) 81 + } 82 + 83 + if exitCode != 0 { 84 + return interp.ExitStatus(uint8(exitCode)) 85 + } 86 + return nil 87 + } 88 + 89 + func writeFile(ec *command.ExecContext, file string, content []byte, appendMode bool) error { 90 + if ec.FS == nil { 91 + return errors.New("no filesystem") 92 + } 93 + full := resolvePath(ec, file) 94 + flag := os.O_CREATE | os.O_WRONLY | os.O_TRUNC 95 + if appendMode { 96 + flag = os.O_CREATE | os.O_WRONLY | os.O_APPEND 97 + } 98 + f, err := ec.FS.OpenFile(full, flag, 0o644) 99 + if err != nil { 100 + return err 101 + } 102 + defer f.Close() 103 + if _, err := f.Write(content); err != nil { 104 + return err 105 + } 106 + return nil 107 + } 108 + 109 + func resolvePath(ec *command.ExecContext, p string) string { 110 + dir := ec.Dir 111 + if dir == "" { 112 + dir = "." 113 + } 114 + if path.IsAbs(p) { 115 + p = strings.TrimPrefix(p, "/") 116 + if p == "" { 117 + return "." 118 + } 119 + return path.Clean(p) 120 + } 121 + joined := path.Join(dir, p) 122 + if joined == "" { 123 + return "." 124 + } 125 + return joined 126 + }
+197
command/internal/tee/tee_test.go
··· 1 + package tee 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "io" 7 + "os" 8 + "strings" 9 + "testing" 10 + 11 + "github.com/go-git/go-billy/v5" 12 + "github.com/go-git/go-billy/v5/memfs" 13 + "tangled.org/xeiaso.net/kefka/command" 14 + ) 15 + 16 + func newFS(t *testing.T) billy.Filesystem { 17 + t.Helper() 18 + fs := memfs.New() 19 + write := func(name string, data []byte) { 20 + f, err := fs.OpenFile(name, os.O_CREATE|os.O_WRONLY, 0o644) 21 + if err != nil { 22 + t.Fatal(err) 23 + } 24 + f.Write(data) 25 + f.Close() 26 + } 27 + write("existing.txt", []byte("old contents\n")) 28 + return fs 29 + } 30 + 31 + func readFile(t *testing.T, fs billy.Filesystem, name string) string { 32 + t.Helper() 33 + f, err := fs.Open(name) 34 + if err != nil { 35 + t.Fatalf("open %s: %v", name, err) 36 + } 37 + defer f.Close() 38 + data, err := io.ReadAll(f) 39 + if err != nil { 40 + t.Fatalf("read %s: %v", name, err) 41 + } 42 + return string(data) 43 + } 44 + 45 + func run(t *testing.T, args []string, stdin string, fs billy.Filesystem) (string, string, error) { 46 + t.Helper() 47 + var stdout, stderr bytes.Buffer 48 + ec := &command.ExecContext{ 49 + Stdin: strings.NewReader(stdin), 50 + Stdout: &stdout, 51 + Stderr: &stderr, 52 + Dir: ".", 53 + FS: fs, 54 + } 55 + err := Impl{}.Exec(context.Background(), ec, args) 56 + return stdout.String(), stderr.String(), err 57 + } 58 + 59 + func TestTee(t *testing.T) { 60 + tests := []struct { 61 + name string 62 + args []string 63 + stdin string 64 + wantStdout string 65 + wantErrSub string 66 + wantErr bool 67 + wantFiles map[string]string 68 + preexisting map[string]string 69 + }{ 70 + { 71 + name: "no files passes stdin to stdout", 72 + args: nil, 73 + stdin: "hello\n", 74 + wantStdout: "hello\n", 75 + }, 76 + { 77 + name: "single file gets stdin and stdout passes through", 78 + args: []string{"out.txt"}, 79 + stdin: "hello\n", 80 + wantStdout: "hello\n", 81 + wantFiles: map[string]string{"out.txt": "hello\n"}, 82 + }, 83 + { 84 + name: "multiple files all receive stdin", 85 + args: []string{"a.txt", "b.txt", "c.txt"}, 86 + stdin: "shared\n", 87 + wantStdout: "shared\n", 88 + wantFiles: map[string]string{ 89 + "a.txt": "shared\n", 90 + "b.txt": "shared\n", 91 + "c.txt": "shared\n", 92 + }, 93 + }, 94 + { 95 + name: "default mode overwrites existing file", 96 + args: []string{"existing.txt"}, 97 + stdin: "new\n", 98 + wantStdout: "new\n", 99 + wantFiles: map[string]string{"existing.txt": "new\n"}, 100 + }, 101 + { 102 + name: "append short flag preserves existing content", 103 + args: []string{"-a", "existing.txt"}, 104 + stdin: "added\n", 105 + wantStdout: "added\n", 106 + wantFiles: map[string]string{"existing.txt": "old contents\nadded\n"}, 107 + }, 108 + { 109 + name: "append long flag preserves existing content", 110 + args: []string{"--append", "existing.txt"}, 111 + stdin: "added\n", 112 + wantStdout: "added\n", 113 + wantFiles: map[string]string{"existing.txt": "old contents\nadded\n"}, 114 + }, 115 + { 116 + name: "empty stdin writes empty file", 117 + args: []string{"empty.txt"}, 118 + stdin: "", 119 + wantStdout: "", 120 + wantFiles: map[string]string{"empty.txt": ""}, 121 + }, 122 + { 123 + name: "binary safe stdin propagates verbatim", 124 + args: []string{"bin.dat"}, 125 + stdin: "a\x00b\xffc", 126 + wantStdout: "a\x00b\xffc", 127 + wantFiles: map[string]string{"bin.dat": "a\x00b\xffc"}, 128 + }, 129 + { 130 + name: "unknown flag errors", 131 + args: []string{"--nope"}, 132 + wantErr: true, 133 + }, 134 + } 135 + 136 + for _, tt := range tests { 137 + t.Run(tt.name, func(t *testing.T) { 138 + fs := newFS(t) 139 + stdout, stderr, err := run(t, tt.args, tt.stdin, fs) 140 + if tt.wantErr { 141 + if err == nil { 142 + t.Fatalf("expected error, got nil; stdout=%q stderr=%q", stdout, stderr) 143 + } 144 + } else if err != nil { 145 + t.Fatalf("unexpected error: %v; stderr=%q", err, stderr) 146 + } 147 + if stdout != tt.wantStdout { 148 + t.Errorf("stdout = %q, want %q", stdout, tt.wantStdout) 149 + } 150 + if tt.wantErrSub != "" && !strings.Contains(stderr, tt.wantErrSub) { 151 + t.Errorf("stderr = %q, want substring %q", stderr, tt.wantErrSub) 152 + } 153 + for name, want := range tt.wantFiles { 154 + if got := readFile(t, fs, name); got != want { 155 + t.Errorf("file %s = %q, want %q", name, got, want) 156 + } 157 + } 158 + }) 159 + } 160 + } 161 + 162 + func TestNilFSWithFiles(t *testing.T) { 163 + var stdout, stderr bytes.Buffer 164 + ec := &command.ExecContext{ 165 + Stdin: strings.NewReader("hi\n"), 166 + Stdout: &stdout, 167 + Stderr: &stderr, 168 + Dir: ".", 169 + FS: nil, 170 + } 171 + err := Impl{}.Exec(context.Background(), ec, []string{"out.txt"}) 172 + if err == nil { 173 + t.Fatalf("expected error when FS is nil and files are given") 174 + } 175 + if stdout.String() != "hi\n" { 176 + t.Errorf("stdout = %q, want %q", stdout.String(), "hi\n") 177 + } 178 + if !strings.Contains(stderr.String(), "tee: out.txt: No such file or directory") { 179 + t.Errorf("stderr missing expected message: %q", stderr.String()) 180 + } 181 + } 182 + 183 + func TestHelp(t *testing.T) { 184 + stdout, stderr, err := run(t, []string{"--help"}, "", newFS(t)) 185 + if err != nil { 186 + t.Fatalf("unexpected error: %v", err) 187 + } 188 + if stdout != "" { 189 + t.Errorf("expected empty stdout, got %q", stdout) 190 + } 191 + if !strings.Contains(stderr, "Usage: tee [OPTION]... [FILE]...") { 192 + t.Errorf("usage line missing from stderr: %q", stderr) 193 + } 194 + if !strings.Contains(stderr, "--append") { 195 + t.Errorf("append flag missing from help: %q", stderr) 196 + } 197 + }