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 od from just-bash

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

Xe Iaso 6426cab0 0843b4f1

+422
+223
command/internal/od/od.go
··· 1 + package od 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "fmt" 7 + "io" 8 + "path" 9 + "slices" 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 + type outputFormat int 20 + 21 + const ( 22 + fmtOctal outputFormat = iota 23 + fmtHex 24 + fmtChar 25 + ) 26 + 27 + func (Impl) Exec(ctx context.Context, ec *command.ExecContext, args []string) error { 28 + if ec == nil { 29 + return errors.New("od: nil ExecContext") 30 + } 31 + 32 + stdout := ec.Stdout 33 + if stdout == nil { 34 + stdout = io.Discard 35 + } 36 + stderr := ec.Stderr 37 + if stderr == nil { 38 + stderr = io.Discard 39 + } 40 + 41 + set := getopt.New() 42 + set.SetProgram("od") 43 + set.SetParameters("[FILE]") 44 + 45 + usage := func() { 46 + fmt.Fprint(stderr, "Usage: od [OPTION]... [FILE]\n") 47 + fmt.Fprint(stderr, "Write an unambiguous representation, octal bytes by default,\n") 48 + fmt.Fprint(stderr, "of FILE to standard output.\n\n") 49 + fmt.Fprint(stderr, " -A, --address-radix=RADIX output format for addresses; n means no address\n") 50 + fmt.Fprint(stderr, " -c same as -t c\n") 51 + fmt.Fprint(stderr, " -t, --format=TYPE select output format; one of x1, c, o*\n") 52 + fmt.Fprint(stderr, " --help display this help and exit\n") 53 + } 54 + set.SetUsage(usage) 55 + 56 + addrOpt := set.StringLong("address-radix", 'A', "o", "output format for addresses; n means no address") 57 + _ = set.Bool('c', "same as -t c") 58 + tList := set.ListLong("format", 't', "select output format; one of x1, c, o*") 59 + helpFlag := set.BoolLong("help", 0, "display this help and exit") 60 + 61 + var formats []outputFormat 62 + tIdx := 0 63 + callback := func(opt getopt.Option) bool { 64 + switch opt.ShortName() { 65 + case "c": 66 + formats = append(formats, fmtChar) 67 + case "t": 68 + if tIdx < len(*tList) { 69 + v := (*tList)[tIdx] 70 + tIdx++ 71 + switch { 72 + case v == "x1": 73 + formats = append(formats, fmtHex) 74 + case v == "c": 75 + formats = append(formats, fmtChar) 76 + case strings.HasPrefix(v, "o"): 77 + formats = append(formats, fmtOctal) 78 + } 79 + } 80 + } 81 + return true 82 + } 83 + 84 + if err := set.Getopt(append([]string{"od"}, args...), callback); err != nil { 85 + fmt.Fprintf(stderr, "od: %s\n", err) 86 + usage() 87 + return interp.ExitStatus(1) 88 + } 89 + 90 + if *helpFlag { 91 + usage() 92 + return nil 93 + } 94 + 95 + addressNone := *addrOpt == "n" 96 + 97 + if len(formats) == 0 { 98 + formats = []outputFormat{fmtOctal} 99 + } 100 + 101 + files := set.Args() 102 + data, err := readInput(ec, files, stderr) 103 + if err != nil { 104 + return err 105 + } 106 + 107 + io.WriteString(stdout, buildOutput(data, formats, addressNone)) 108 + return nil 109 + } 110 + 111 + func readInput(ec *command.ExecContext, files []string, stderr io.Writer) ([]byte, error) { 112 + if len(files) == 0 || files[0] == "-" { 113 + if ec.Stdin == nil { 114 + return nil, nil 115 + } 116 + return io.ReadAll(ec.Stdin) 117 + } 118 + if ec.FS == nil { 119 + fmt.Fprintf(stderr, "od: %s: No such file or directory\n", files[0]) 120 + return nil, interp.ExitStatus(1) 121 + } 122 + full := resolvePath(ec, files[0]) 123 + f, err := ec.FS.Open(full) 124 + if err != nil { 125 + fmt.Fprintf(stderr, "od: %s: No such file or directory\n", files[0]) 126 + return nil, interp.ExitStatus(1) 127 + } 128 + defer f.Close() 129 + return io.ReadAll(f) 130 + } 131 + 132 + func buildOutput(data []byte, formats []outputFormat, addressNone bool) string { 133 + if len(data) == 0 { 134 + return "" 135 + } 136 + 137 + hasCharFormat := slices.Contains(formats, fmtChar) 138 + 139 + const bytesPerLine = 16 140 + var b strings.Builder 141 + 142 + for offset := 0; offset < len(data); offset += bytesPerLine { 143 + chunk := data[offset:min(offset+bytesPerLine, len(data))] 144 + 145 + for fIdx, f := range formats { 146 + switch { 147 + case fIdx == 0 && !addressNone: 148 + fmt.Fprintf(&b, "%07o ", offset) 149 + case fIdx > 0 && !addressNone: 150 + b.WriteString(" ") 151 + } 152 + 153 + for _, code := range chunk { 154 + switch f { 155 + case fmtChar: 156 + b.WriteString(formatCharByte(code)) 157 + case fmtHex: 158 + b.WriteString(formatHexByte(code, hasCharFormat)) 159 + case fmtOctal: 160 + fmt.Fprintf(&b, " %03o", code) 161 + } 162 + } 163 + b.WriteByte('\n') 164 + } 165 + } 166 + 167 + if !addressNone { 168 + fmt.Fprintf(&b, "%07o\n", len(data)) 169 + } 170 + 171 + return b.String() 172 + } 173 + 174 + func formatCharByte(code byte) string { 175 + switch code { 176 + case 0: 177 + return ` \0` 178 + case 7: 179 + return ` \a` 180 + case 8: 181 + return ` \b` 182 + case 9: 183 + return ` \t` 184 + case 10: 185 + return ` \n` 186 + case 11: 187 + return ` \v` 188 + case 12: 189 + return ` \f` 190 + case 13: 191 + return ` \r` 192 + } 193 + if code >= 32 && code < 127 { 194 + return fmt.Sprintf(" %c", code) 195 + } 196 + return fmt.Sprintf(" %03o", code) 197 + } 198 + 199 + func formatHexByte(code byte, padForChar bool) string { 200 + if padForChar { 201 + return fmt.Sprintf(" %02x", code) 202 + } 203 + return fmt.Sprintf(" %02x", code) 204 + } 205 + 206 + func resolvePath(ec *command.ExecContext, p string) string { 207 + dir := ec.Dir 208 + if dir == "" { 209 + dir = "." 210 + } 211 + if path.IsAbs(p) { 212 + p = strings.TrimPrefix(p, "/") 213 + if p == "" { 214 + return "." 215 + } 216 + return path.Clean(p) 217 + } 218 + joined := path.Join(dir, p) 219 + if joined == "" { 220 + return "." 221 + } 222 + return joined 223 + }
+199
command/internal/od/od_test.go
··· 1 + package od 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "os" 7 + "strings" 8 + "testing" 9 + 10 + "github.com/go-git/go-billy/v5" 11 + "github.com/go-git/go-billy/v5/memfs" 12 + "tangled.org/xeiaso.net/kefka/command" 13 + ) 14 + 15 + func newFS(t *testing.T) billy.Filesystem { 16 + t.Helper() 17 + fs := memfs.New() 18 + write := func(name string, data []byte) { 19 + f, err := fs.OpenFile(name, os.O_CREATE|os.O_WRONLY, 0o644) 20 + if err != nil { 21 + t.Fatal(err) 22 + } 23 + f.Write(data) 24 + f.Close() 25 + } 26 + write("hi.txt", []byte("Hi")) 27 + write("ab.bin", []byte{0x01, 0x41, 0x7f, 0xff}) 28 + return fs 29 + } 30 + 31 + func run(t *testing.T, args []string, stdin string, fs billy.Filesystem) (string, string, error) { 32 + t.Helper() 33 + var stdout, stderr bytes.Buffer 34 + ec := &command.ExecContext{ 35 + Stdin: strings.NewReader(stdin), 36 + Stdout: &stdout, 37 + Stderr: &stderr, 38 + Dir: ".", 39 + FS: fs, 40 + } 41 + err := Impl{}.Exec(context.Background(), ec, args) 42 + return stdout.String(), stderr.String(), err 43 + } 44 + 45 + func TestOd(t *testing.T) { 46 + tests := []struct { 47 + name string 48 + args []string 49 + stdin string 50 + wantStdout string 51 + wantErrSub string 52 + wantErr bool 53 + }{ 54 + { 55 + name: "default octal from stdin", 56 + args: nil, 57 + stdin: "Hi", 58 + wantStdout: "0000000 110 151\n0000002\n", 59 + }, 60 + { 61 + name: "default octal from file", 62 + args: []string{"hi.txt"}, 63 + wantStdout: "0000000 110 151\n0000002\n", 64 + }, 65 + { 66 + name: "dash means stdin", 67 + args: []string{"-"}, 68 + stdin: "Hi", 69 + wantStdout: "0000000 110 151\n0000002\n", 70 + }, 71 + { 72 + name: "empty input produces no output", 73 + args: nil, 74 + stdin: "", 75 + wantStdout: "", 76 + }, 77 + { 78 + name: "char format printable", 79 + args: []string{"-c"}, 80 + stdin: "ABC", 81 + wantStdout: "0000000 A B C\n0000003\n", 82 + }, 83 + { 84 + name: "char format named escape", 85 + args: []string{"-c"}, 86 + stdin: "A\nB", 87 + wantStdout: "0000000 A \\n B\n0000003\n", 88 + }, 89 + { 90 + name: "char format octal fallback for non-printable", 91 + args: []string{"-c", "ab.bin"}, 92 + wantStdout: "0000000 001 A 177 377\n0000004\n", 93 + }, 94 + { 95 + name: "hex via -t x1", 96 + args: []string{"-t", "x1"}, 97 + stdin: "Hi", 98 + wantStdout: "0000000 48 69\n0000002\n", 99 + }, 100 + { 101 + name: "octal via -t o", 102 + args: []string{"-t", "o"}, 103 + stdin: "Hi", 104 + wantStdout: "0000000 110 151\n0000002\n", 105 + }, 106 + { 107 + name: "char via -t c", 108 + args: []string{"-t", "c"}, 109 + stdin: "AB", 110 + wantStdout: "0000000 A B\n0000002\n", 111 + }, 112 + { 113 + name: "no address with -An", 114 + args: []string{"-An", "-c"}, 115 + stdin: "AB", 116 + wantStdout: " A B\n", 117 + }, 118 + { 119 + name: "no address with separated -A n", 120 + args: []string{"-A", "n", "-c"}, 121 + stdin: "AB", 122 + wantStdout: " A B\n", 123 + }, 124 + { 125 + name: "char and hex together widens hex field", 126 + args: []string{"-c", "-t", "x1"}, 127 + stdin: "A", 128 + wantStdout: "0000000 A\n 41\n0000001\n", 129 + }, 130 + { 131 + name: "format order is preserved", 132 + args: []string{"-t", "x1", "-c"}, 133 + stdin: "A", 134 + wantStdout: "0000000 41\n A\n0000001\n", 135 + }, 136 + { 137 + name: "wraps after 16 bytes per line", 138 + args: []string{"-t", "x1"}, 139 + stdin: "0123456789abcdefXY", 140 + wantStdout: "0000000 30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66\n0000020 58 59\n0000022\n", 141 + }, 142 + { 143 + name: "unrecognized -t format is silently ignored", 144 + args: []string{"-t", "d"}, 145 + stdin: "Hi", 146 + wantStdout: "0000000 110 151\n0000002\n", 147 + }, 148 + { 149 + name: "missing file reports error", 150 + args: []string{"nope.bin"}, 151 + wantStdout: "", 152 + wantErrSub: "od: nope.bin: No such file or directory", 153 + wantErr: true, 154 + }, 155 + { 156 + name: "unknown flag returns error", 157 + args: []string{"--no-such-flag"}, 158 + wantErr: true, 159 + }, 160 + } 161 + 162 + for _, tt := range tests { 163 + t.Run(tt.name, func(t *testing.T) { 164 + stdout, stderr, err := run(t, tt.args, tt.stdin, newFS(t)) 165 + if tt.wantErr { 166 + if err == nil { 167 + t.Fatalf("expected error, got nil; stdout=%q stderr=%q", stdout, stderr) 168 + } 169 + } else if err != nil { 170 + t.Fatalf("unexpected error: %v; stderr=%q", err, stderr) 171 + } 172 + if stdout != tt.wantStdout { 173 + t.Errorf("stdout mismatch\n got: %q\nwant: %q", stdout, tt.wantStdout) 174 + } 175 + if tt.wantErrSub != "" && !strings.Contains(stderr, tt.wantErrSub) { 176 + t.Errorf("stderr = %q, want substring %q", stderr, tt.wantErrSub) 177 + } 178 + }) 179 + } 180 + } 181 + 182 + func TestHelp(t *testing.T) { 183 + stdout, stderr, err := run(t, []string{"--help"}, "", newFS(t)) 184 + if err != nil { 185 + t.Fatalf("unexpected error: %v", err) 186 + } 187 + if stdout != "" { 188 + t.Errorf("expected empty stdout, got %q", stdout) 189 + } 190 + if !strings.Contains(stderr, "Usage: od [OPTION]... [FILE]") { 191 + t.Errorf("usage line missing from stderr: %q", stderr) 192 + } 193 + if !strings.Contains(stderr, "-A, --address-radix") { 194 + t.Errorf("address-radix flag missing from help: %q", stderr) 195 + } 196 + if !strings.Contains(stderr, "-t, --format") { 197 + t.Errorf("format flag missing from help: %q", stderr) 198 + } 199 + }