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

Port the base64 coreutil from just-bash. Supports encode/decode with
-d/--decode, line wrapping via -w/--wrap, reading from stdin or files,
and "-" for stdin. Uses pborman/getopt for GNU-style option parsing
to match the reference behavior.

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

Xe Iaso fcdeacbc 85a08fac

+381
+174
command/internal/base64/base64.go
··· 1 + package base64 2 + 3 + import ( 4 + "context" 5 + enc "encoding/base64" 6 + "errors" 7 + "fmt" 8 + "io" 9 + "path" 10 + "strings" 11 + "unicode" 12 + 13 + "github.com/pborman/getopt/v2" 14 + "mvdan.cc/sh/v3/interp" 15 + "tangled.org/xeiaso.net/kefka/command" 16 + ) 17 + 18 + type Impl struct{} 19 + 20 + func (Impl) Exec(ctx context.Context, ec *command.ExecContext, args []string) error { 21 + if ec == nil { 22 + return errors.New("base64: nil ExecContext") 23 + } 24 + 25 + stdout := ec.Stdout 26 + if stdout == nil { 27 + stdout = io.Discard 28 + } 29 + stderr := ec.Stderr 30 + if stderr == nil { 31 + stderr = io.Discard 32 + } 33 + 34 + set := getopt.New() 35 + set.SetProgram("base64") 36 + set.SetParameters("[FILE]") 37 + 38 + usage := func() { 39 + fmt.Fprint(stderr, "Usage: base64 [OPTION]... [FILE]\n") 40 + fmt.Fprint(stderr, "Base64 encode or decode FILE, or standard input, to standard output.\n\n") 41 + fmt.Fprint(stderr, " -d, --decode decode data\n") 42 + fmt.Fprint(stderr, " -w, --wrap=COLS wrap encoded lines after COLS character (default 76, 0 to disable)\n") 43 + fmt.Fprint(stderr, " --help display this help and exit\n") 44 + } 45 + set.SetUsage(usage) 46 + 47 + decode := set.BoolLong("decode", 'd', "decode data") 48 + wrapCols := set.IntLong("wrap", 'w', 76, "wrap encoded lines after COLS character (default 76, 0 to disable)") 49 + help := set.BoolLong("help", 0, "display this help and exit") 50 + 51 + if err := set.Getopt(append([]string{"base64"}, args...), nil); err != nil { 52 + fmt.Fprintf(stderr, "base64: %s\n", err) 53 + usage() 54 + return interp.ExitStatus(1) 55 + } 56 + 57 + if *help { 58 + usage() 59 + return nil 60 + } 61 + 62 + files := set.Args() 63 + 64 + data, err := readInput(ec, files, stderr) 65 + if err != nil { 66 + return err 67 + } 68 + 69 + if *decode { 70 + cleaned := stripWhitespace(data) 71 + decoded, err := enc.StdEncoding.DecodeString(string(cleaned)) 72 + if err != nil { 73 + fmt.Fprint(stderr, "base64: invalid input\n") 74 + return interp.ExitStatus(1) 75 + } 76 + stdout.Write(decoded) 77 + return nil 78 + } 79 + 80 + encoded := enc.StdEncoding.EncodeToString(data) 81 + if *wrapCols > 0 { 82 + encoded = wrapLines(encoded, *wrapCols) 83 + } 84 + io.WriteString(stdout, encoded) 85 + return nil 86 + } 87 + 88 + func readInput(ec *command.ExecContext, files []string, stderr io.Writer) ([]byte, error) { 89 + if len(files) == 0 || (len(files) == 1 && files[0] == "-") { 90 + return readStdin(ec) 91 + } 92 + 93 + var out []byte 94 + for _, file := range files { 95 + if file == "-" { 96 + data, err := readStdin(ec) 97 + if err != nil { 98 + return nil, err 99 + } 100 + out = append(out, data...) 101 + continue 102 + } 103 + if ec.FS == nil { 104 + fmt.Fprintf(stderr, "base64: %s: No such file or directory\n", file) 105 + return nil, interp.ExitStatus(1) 106 + } 107 + full := resolvePath(ec, file) 108 + f, err := ec.FS.Open(full) 109 + if err != nil { 110 + fmt.Fprintf(stderr, "base64: %s: No such file or directory\n", file) 111 + return nil, interp.ExitStatus(1) 112 + } 113 + data, err := io.ReadAll(f) 114 + f.Close() 115 + if err != nil { 116 + fmt.Fprintf(stderr, "base64: %s: %v\n", file, err) 117 + return nil, interp.ExitStatus(1) 118 + } 119 + out = append(out, data...) 120 + } 121 + return out, nil 122 + } 123 + 124 + func readStdin(ec *command.ExecContext) ([]byte, error) { 125 + if ec.Stdin == nil { 126 + return nil, nil 127 + } 128 + return io.ReadAll(ec.Stdin) 129 + } 130 + 131 + func stripWhitespace(b []byte) []byte { 132 + out := make([]byte, 0, len(b)) 133 + for _, r := range string(b) { 134 + if !unicode.IsSpace(r) { 135 + out = append(out, byte(r)) 136 + } 137 + } 138 + return out 139 + } 140 + 141 + func wrapLines(s string, cols int) string { 142 + if cols <= 0 || len(s) == 0 { 143 + return s 144 + } 145 + var b strings.Builder 146 + for i := 0; i < len(s); i += cols { 147 + end := min(i+cols, len(s)) 148 + if i > 0 { 149 + b.WriteByte('\n') 150 + } 151 + b.WriteString(s[i:end]) 152 + } 153 + b.WriteByte('\n') 154 + return b.String() 155 + } 156 + 157 + func resolvePath(ec *command.ExecContext, p string) string { 158 + dir := ec.Dir 159 + if dir == "" { 160 + dir = "." 161 + } 162 + if path.IsAbs(p) { 163 + p = strings.TrimPrefix(p, "/") 164 + if p == "" { 165 + return "." 166 + } 167 + return path.Clean(p) 168 + } 169 + joined := path.Join(dir, p) 170 + if joined == "" { 171 + return "." 172 + } 173 + return joined 174 + }
+202
command/internal/base64/base64_test.go
··· 1 + package base64 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("hello.txt", []byte("hello")) 27 + write("two.txt", []byte("world")) 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 TestEncodeStdin(t *testing.T) { 46 + stdout, stderr, err := run(t, nil, "hello", newFS(t)) 47 + if err != nil { 48 + t.Fatalf("unexpected error: %v", err) 49 + } 50 + if stderr != "" { 51 + t.Fatalf("unexpected stderr: %q", stderr) 52 + } 53 + if want := "aGVsbG8=\n"; stdout != want { 54 + t.Errorf("stdout = %q, want %q", stdout, want) 55 + } 56 + } 57 + 58 + func TestEncodeFile(t *testing.T) { 59 + stdout, _, err := run(t, []string{"hello.txt"}, "", newFS(t)) 60 + if err != nil { 61 + t.Fatalf("unexpected error: %v", err) 62 + } 63 + if want := "aGVsbG8=\n"; stdout != want { 64 + t.Errorf("stdout = %q, want %q", stdout, want) 65 + } 66 + } 67 + 68 + func TestEncodeMultipleFiles(t *testing.T) { 69 + stdout, _, err := run(t, []string{"hello.txt", "two.txt"}, "", newFS(t)) 70 + if err != nil { 71 + t.Fatalf("unexpected error: %v", err) 72 + } 73 + if want := "aGVsbG93b3JsZA==\n"; stdout != want { 74 + t.Errorf("stdout = %q, want %q", stdout, want) 75 + } 76 + } 77 + 78 + func TestEncodeStdinDash(t *testing.T) { 79 + stdout, _, err := run(t, []string{"-"}, "hello", newFS(t)) 80 + if err != nil { 81 + t.Fatalf("unexpected error: %v", err) 82 + } 83 + if want := "aGVsbG8=\n"; stdout != want { 84 + t.Errorf("stdout = %q, want %q", stdout, want) 85 + } 86 + } 87 + 88 + func TestEncodeWrap(t *testing.T) { 89 + // 80 'a' bytes encodes to 108 chars; wrap=10 should split. 90 + input := strings.Repeat("a", 30) 91 + stdout, _, err := run(t, []string{"-w", "10"}, input, newFS(t)) 92 + if err != nil { 93 + t.Fatalf("unexpected error: %v", err) 94 + } 95 + for _, line := range strings.Split(strings.TrimRight(stdout, "\n"), "\n") { 96 + if len(line) > 10 { 97 + t.Errorf("line too long (%d): %q", len(line), line) 98 + } 99 + } 100 + if !strings.HasSuffix(stdout, "\n") { 101 + t.Errorf("output should end with newline, got %q", stdout) 102 + } 103 + } 104 + 105 + func TestEncodeWrapZeroDisablesWrap(t *testing.T) { 106 + input := strings.Repeat("a", 30) 107 + stdout, _, err := run(t, []string{"-w", "0"}, input, newFS(t)) 108 + if err != nil { 109 + t.Fatalf("unexpected error: %v", err) 110 + } 111 + if strings.Contains(stdout, "\n") { 112 + t.Errorf("expected no newlines with -w 0, got %q", stdout) 113 + } 114 + } 115 + 116 + func TestEncodeEmpty(t *testing.T) { 117 + stdout, _, err := run(t, nil, "", newFS(t)) 118 + if err != nil { 119 + t.Fatalf("unexpected error: %v", err) 120 + } 121 + if stdout != "" { 122 + t.Errorf("expected empty stdout, got %q", stdout) 123 + } 124 + } 125 + 126 + func TestDecodeStdin(t *testing.T) { 127 + stdout, _, err := run(t, []string{"-d"}, "aGVsbG8=", newFS(t)) 128 + if err != nil { 129 + t.Fatalf("unexpected error: %v", err) 130 + } 131 + if want := "hello"; stdout != want { 132 + t.Errorf("stdout = %q, want %q", stdout, want) 133 + } 134 + } 135 + 136 + func TestDecodeStripsWhitespace(t *testing.T) { 137 + stdout, _, err := run(t, []string{"-d"}, "aGVs\nbG8=\n", newFS(t)) 138 + if err != nil { 139 + t.Fatalf("unexpected error: %v", err) 140 + } 141 + if want := "hello"; stdout != want { 142 + t.Errorf("stdout = %q, want %q", stdout, want) 143 + } 144 + } 145 + 146 + func TestDecodeInvalid(t *testing.T) { 147 + _, stderr, err := run(t, []string{"--decode"}, "not!valid!base64!@#", newFS(t)) 148 + if err == nil { 149 + t.Fatal("expected error for invalid input") 150 + } 151 + if !strings.Contains(stderr, "invalid input") { 152 + t.Errorf("stderr = %q, want 'invalid input'", stderr) 153 + } 154 + } 155 + 156 + func TestMissingFile(t *testing.T) { 157 + _, stderr, err := run(t, []string{"nope.txt"}, "", newFS(t)) 158 + if err == nil { 159 + t.Fatal("expected error for missing file") 160 + } 161 + if !strings.Contains(stderr, "No such file or directory") { 162 + t.Errorf("stderr = %q", stderr) 163 + } 164 + } 165 + 166 + func TestHelp(t *testing.T) { 167 + _, stderr, err := run(t, []string{"--help"}, "", newFS(t)) 168 + if err != nil { 169 + t.Fatalf("unexpected error: %v", err) 170 + } 171 + if !strings.Contains(stderr, "Usage: base64 [OPTION]... [FILE]") { 172 + t.Errorf("usage missing from stderr: %q", stderr) 173 + } 174 + if !strings.Contains(stderr, "-d, --decode") { 175 + t.Errorf("decode flag missing from help: %q", stderr) 176 + } 177 + if !strings.Contains(stderr, "-w, --wrap=COLS") { 178 + t.Errorf("wrap flag missing from help: %q", stderr) 179 + } 180 + } 181 + 182 + func TestUnknownFlag(t *testing.T) { 183 + _, _, err := run(t, []string{"--no-such-flag"}, "", newFS(t)) 184 + if err == nil { 185 + t.Fatal("expected error for unknown flag") 186 + } 187 + } 188 + 189 + func TestRoundTrip(t *testing.T) { 190 + input := "Hello, \xff\xfe World! \x00\x01\x02" 191 + enc, _, err := run(t, nil, input, newFS(t)) 192 + if err != nil { 193 + t.Fatalf("encode: %v", err) 194 + } 195 + dec, _, err := run(t, []string{"-d"}, enc, newFS(t)) 196 + if err != nil { 197 + t.Fatalf("decode: %v", err) 198 + } 199 + if dec != input { 200 + t.Errorf("round trip mismatch: want %q, got %q", input, dec) 201 + } 202 + }
+2
command/registry/coreutils/coreutils.go
··· 1 1 package coreutils 2 2 3 3 import ( 4 + "tangled.org/xeiaso.net/kefka/command/internal/base64" 4 5 "tangled.org/xeiaso.net/kefka/command/internal/falsecmd" 5 6 "tangled.org/xeiaso.net/kefka/command/internal/hostname" 6 7 "tangled.org/xeiaso.net/kefka/command/internal/ls" ··· 9 10 ) 10 11 11 12 func Register(reg *registry.Impl) { 13 + reg.Register("base64", base64.Impl{}) 12 14 reg.Register("false", falsecmd.Impl{}) 13 15 reg.Register("hostname", hostname.Impl{}) 14 16 reg.Register("ls", ls.Impl{})
+1
go.mod
··· 4 4 5 5 require ( 6 6 github.com/go-git/go-billy/v5 v5.8.0 7 + github.com/pborman/getopt/v2 v2.1.0 7 8 github.com/spf13/pflag v1.0.10 8 9 github.com/tetratelabs/wazero v1.11.0 9 10 golang.org/x/term v0.41.0
+2
go.sum
··· 16 16 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 17 17 github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 18 18 github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 19 + github.com/pborman/getopt/v2 v2.1.0 h1:eNfR+r+dWLdWmV8g5OlpyrTYHkhVNxHBdN2cCrJmOEA= 20 + github.com/pborman/getopt/v2 v2.1.0/go.mod h1:4NtW75ny4eBw9fO1bhtNdYTlZKYX5/tBLtsOpwKIKd0= 19 21 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 22 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 23 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=