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 zcat from just-bash and register gzip/gunzip/zcat

Standalone decompression-to-stdout command. Always reads .gz files and
writes decompressed output to stdout without modifying originals.
Supports integrity testing with -t, listing with -l, verbose output,
and custom suffixes with -S.

Also registers gzip, gunzip, and zcat in the coreutils registry.

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

Xe Iaso ccf5721a a61fb36f

+511
+248
command/internal/zcat/zcat.go
··· 1 + package zcat 2 + 3 + import ( 4 + "bytes" 5 + gzlib "compress/gzip" 6 + "context" 7 + "errors" 8 + "fmt" 9 + "io" 10 + "os" 11 + "path" 12 + "strings" 13 + 14 + "github.com/pborman/getopt/v2" 15 + "mvdan.cc/sh/v3/interp" 16 + "tangled.org/xeiaso.net/kefka/command" 17 + ) 18 + 19 + type Impl struct{} 20 + 21 + func (Impl) Exec(ctx context.Context, ec *command.ExecContext, args []string) error { 22 + if ec == nil { 23 + return errors.New("zcat: nil ExecContext") 24 + } 25 + 26 + stdout := ec.Stdout 27 + if stdout == nil { 28 + stdout = io.Discard 29 + } 30 + stderr := ec.Stderr 31 + if stderr == nil { 32 + stderr = io.Discard 33 + } 34 + 35 + set := getopt.New() 36 + set.SetProgram("zcat") 37 + set.SetParameters("[FILE]...") 38 + 39 + usage := func() { 40 + fmt.Fprint(stderr, "Usage: zcat [OPTION]... [FILE]...\n") 41 + fmt.Fprint(stderr, "Decompress FILEs to standard output.\n\n") 42 + fmt.Fprint(stderr, "When no FILE is given, or when FILE is -, read from standard input.\n\n") 43 + fmt.Fprint(stderr, " -f, --force force; read compressed data even from a terminal\n") 44 + fmt.Fprint(stderr, " -l, --list list compressed file contents\n") 45 + fmt.Fprint(stderr, " -q, --quiet suppress all warnings\n") 46 + fmt.Fprint(stderr, " -S, --suffix=SUF use suffix SUF on compressed files (default: .gz)\n") 47 + fmt.Fprint(stderr, " -t, --test test compressed file integrity\n") 48 + fmt.Fprint(stderr, " -v, --verbose verbose mode\n") 49 + fmt.Fprint(stderr, " --help display this help and exit\n") 50 + } 51 + set.SetUsage(usage) 52 + 53 + _ = set.BoolLong("force", 'f', "force; read compressed data even from a terminal") 54 + listFlag := set.BoolLong("list", 'l', "list compressed file contents") 55 + quietFlag := set.BoolLong("quiet", 'q', "suppress all warnings") 56 + _ = set.StringLong("suffix", 'S', ".gz", "use suffix SUF on compressed files (default: .gz)") 57 + testFlag := set.BoolLong("test", 't', "test compressed file integrity") 58 + verboseFlag := set.BoolLong("verbose", 'v', "verbose mode") 59 + helpFlag := set.BoolLong("help", 0, "display this help and exit") 60 + 61 + if err := set.Getopt(append([]string{"zcat"}, args...), nil); err != nil { 62 + fmt.Fprintf(stderr, "zcat: %s\n", err) 63 + usage() 64 + return interp.ExitStatus(1) 65 + } 66 + 67 + if *helpFlag { 68 + usage() 69 + return nil 70 + } 71 + 72 + files := set.Args() 73 + 74 + if len(files) == 0 { 75 + files = []string{"-"} 76 + } 77 + 78 + if *listFlag { 79 + return listFiles(ec, files, *quietFlag, *verboseFlag, stderr) 80 + } 81 + 82 + if *testFlag { 83 + return testFiles(ec, files, *quietFlag, *verboseFlag, stderr) 84 + } 85 + 86 + for _, file := range files { 87 + data, err := readFile(ec, file) 88 + if err != nil { 89 + fmt.Fprintf(stderr, "zcat: %s: %v\n", file, err) 90 + return interp.ExitStatus(1) 91 + } 92 + 93 + if !isGzip(data) { 94 + if !*quietFlag { 95 + fmt.Fprintf(stderr, "zcat: %s: not in gzip format\n", file) 96 + } 97 + return interp.ExitStatus(1) 98 + } 99 + 100 + decompressed, err := decompressData(data) 101 + if err != nil { 102 + if !*quietFlag { 103 + fmt.Fprintf(stderr, "zcat: %s: %v\n", file, err) 104 + } 105 + return interp.ExitStatus(1) 106 + } 107 + 108 + stdout.Write(decompressed) 109 + if *verboseFlag { 110 + fmt.Fprintf(stderr, "%s:\tOK\n", file) 111 + } 112 + } 113 + 114 + return nil 115 + } 116 + 117 + func readFile(ec *command.ExecContext, file string) ([]byte, error) { 118 + if ec.Stdin != nil && file == "-" { 119 + return io.ReadAll(ec.Stdin) 120 + } 121 + 122 + if ec.FS == nil { 123 + return nil, fmt.Errorf("no filesystem") 124 + } 125 + 126 + full := resolvePath(ec, file) 127 + f, err := ec.FS.Open(full) 128 + if err != nil { 129 + if os.IsNotExist(err) { 130 + return nil, fmt.Errorf("No such file or directory") 131 + } 132 + return nil, err 133 + } 134 + defer f.Close() 135 + 136 + data, err := io.ReadAll(f) 137 + if err != nil { 138 + return nil, err 139 + } 140 + 141 + return data, nil 142 + } 143 + 144 + func listFiles(ec *command.ExecContext, files []string, quiet, verbose bool, stderr io.Writer) error { 145 + fmt.Fprintf(stderr, " compressed uncompressed ratio uncompressed_name\n") 146 + 147 + for _, file := range files { 148 + data, err := readFile(ec, file) 149 + if err != nil { 150 + if !quiet { 151 + fmt.Fprintf(stderr, "zcat: %s: %v\n", file, err) 152 + } 153 + continue 154 + } 155 + 156 + if !isGzip(data) { 157 + if !quiet { 158 + fmt.Fprintf(stderr, "zcat: %s: not in gzip format\n", file) 159 + } 160 + continue 161 + } 162 + 163 + compressed := len(data) 164 + uncompressed := uncompressedSize(data) 165 + 166 + ratio := "0.0" 167 + if uncompressed > 0 { 168 + ratio = fmt.Sprintf("%.1f", (1.0-float64(compressed)/float64(uncompressed))*100.0) 169 + } 170 + 171 + fmt.Fprintf(stderr, "%10d %10d %5s%% %s\n", compressed, uncompressed, ratio, file) 172 + 173 + if verbose { 174 + fmt.Fprintf(stderr, "%s:\tOK\n", file) 175 + } 176 + } 177 + 178 + return nil 179 + } 180 + 181 + func testFiles(ec *command.ExecContext, files []string, quiet, verbose bool, stderr io.Writer) error { 182 + for _, file := range files { 183 + data, err := readFile(ec, file) 184 + if err != nil { 185 + fmt.Fprintf(stderr, "zcat: %s: %v\n", file, err) 186 + return interp.ExitStatus(1) 187 + } 188 + 189 + if !isGzip(data) { 190 + if !quiet { 191 + fmt.Fprintf(stderr, "zcat: %s: not in gzip format\n", file) 192 + } 193 + return interp.ExitStatus(1) 194 + } 195 + 196 + _, err = decompressData(data) 197 + if err != nil { 198 + fmt.Fprintf(stderr, "zcat: %s: %v\n", file, err) 199 + return interp.ExitStatus(1) 200 + } 201 + 202 + if verbose { 203 + fmt.Fprintf(stderr, "%s:\tOK\n", file) 204 + } 205 + } 206 + 207 + return nil 208 + } 209 + 210 + func decompressData(data []byte) ([]byte, error) { 211 + r, err := gzlib.NewReader(bytes.NewReader(data)) 212 + if err != nil { 213 + return nil, err 214 + } 215 + defer r.Close() 216 + return io.ReadAll(r) 217 + } 218 + 219 + func isGzip(data []byte) bool { 220 + return len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b 221 + } 222 + 223 + func uncompressedSize(data []byte) int { 224 + if len(data) < 4 { 225 + return 0 226 + } 227 + n := len(data) 228 + return int(data[n-4]) | int(data[n-3])<<8 | int(data[n-2])<<16 | int(data[n-1])<<24 229 + } 230 + 231 + func resolvePath(ec *command.ExecContext, p string) string { 232 + dir := ec.Dir 233 + if dir == "" { 234 + dir = "." 235 + } 236 + if path.IsAbs(p) { 237 + p = strings.TrimPrefix(p, "/") 238 + if p == "" { 239 + return "." 240 + } 241 + return path.Clean(p) 242 + } 243 + joined := path.Join(dir, p) 244 + if joined == "" { 245 + return "." 246 + } 247 + return joined 248 + }
+255
command/internal/zcat/zcat_test.go
··· 1 + package zcat 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "os" 7 + "testing" 8 + 9 + "github.com/go-git/go-billy/v5" 10 + "github.com/go-git/go-billy/v5/memfs" 11 + gzlib "compress/gzip" 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.gz", gzipData(t, []byte("hello world"))) 27 + write("notgz.txt", []byte("not compressed")) 28 + return fs 29 + } 30 + 31 + func gzipData(t *testing.T, data []byte) []byte { 32 + t.Helper() 33 + var buf bytes.Buffer 34 + w := gzlib.NewWriter(&buf) 35 + w.Write(data) 36 + w.Close() 37 + return buf.Bytes() 38 + } 39 + 40 + func run(t *testing.T, args []string, stdin []byte, fs billy.Filesystem) (stdout, stderr []byte, err error) { 41 + t.Helper() 42 + var outb, errb bytes.Buffer 43 + ec := &command.ExecContext{ 44 + Stdin: bytes.NewReader(stdin), 45 + Stdout: &outb, 46 + Stderr: &errb, 47 + Dir: ".", 48 + FS: fs, 49 + } 50 + err = Impl{}.Exec(context.Background(), ec, args) 51 + return outb.Bytes(), errb.Bytes(), err 52 + } 53 + 54 + func TestDecompressStdin(t *testing.T) { 55 + stdout, stderr, err := run(t, nil, gzipData(t, []byte("hello")), newFS(t)) 56 + if err != nil { 57 + t.Fatalf("unexpected error: %v", err) 58 + } 59 + if len(stderr) > 0 { 60 + t.Fatalf("unexpected stderr: %q", stderr) 61 + } 62 + if want := "hello"; string(stdout) != want { 63 + t.Errorf("stdout = %q, want %q", stdout, want) 64 + } 65 + } 66 + 67 + func TestDecompressFile(t *testing.T) { 68 + stdout, stderr, err := run(t, []string{"hello.txt.gz"}, nil, newFS(t)) 69 + if err != nil { 70 + t.Fatalf("unexpected error: %v", err) 71 + } 72 + if len(stderr) > 0 { 73 + t.Fatalf("unexpected stderr: %q", stderr) 74 + } 75 + if want := "hello world"; string(stdout) != want { 76 + t.Errorf("stdout = %q, want %q", stdout, want) 77 + } 78 + 79 + fs := newFS(t) 80 + _, err = fs.Stat("hello.txt.gz") 81 + if err != nil { 82 + t.Fatalf("original file should still exist: %v", err) 83 + } 84 + } 85 + 86 + func TestDecompressMultipleFiles(t *testing.T) { 87 + fs := memfs.New() 88 + write := func(name string, data []byte) { 89 + f, _ := fs.OpenFile(name, os.O_CREATE|os.O_WRONLY, 0o644) 90 + f.Write(data) 91 + f.Close() 92 + } 93 + write("first.gz", gzipData(t, []byte("foo"))) 94 + write("second.gz", gzipData(t, []byte("bar"))) 95 + 96 + stdout, stderr, err := run(t, []string{"first.gz", "second.gz"}, nil, fs) 97 + if err != nil { 98 + t.Fatalf("unexpected error: %v", err) 99 + } 100 + if len(stderr) > 0 { 101 + t.Fatalf("unexpected stderr: %q", stderr) 102 + } 103 + if want := "foobar"; string(stdout) != want { 104 + t.Errorf("stdout = %q, want %q", stdout, want) 105 + } 106 + } 107 + 108 + func TestDashForStdin(t *testing.T) { 109 + stdout, stderr, err := run(t, []string{"-"}, gzipData(t, []byte("hello world")), newFS(t)) 110 + if err != nil { 111 + t.Fatalf("unexpected error: %v", err) 112 + } 113 + if len(stderr) > 0 { 114 + t.Fatalf("unexpected stderr: %q", stderr) 115 + } 116 + if want := "hello world"; string(stdout) != want { 117 + t.Errorf("stdout = %q, want %q", stdout, want) 118 + } 119 + } 120 + 121 + func TestListMode(t *testing.T) { 122 + stdout, stderr, err := run(t, []string{"-l", "hello.txt.gz"}, nil, newFS(t)) 123 + if err != nil { 124 + t.Fatalf("unexpected error: %v", err) 125 + } 126 + stderrStr := string(stderr) 127 + if !contains(stderrStr, "compressed") { 128 + t.Errorf("stderr should contain header: %q", stderrStr) 129 + } 130 + if !contains(stderrStr, "hello.txt.gz") { 131 + t.Errorf("stderr should contain filename: %q", stderrStr) 132 + } 133 + if len(stdout) > 0 { 134 + t.Errorf("stdout should be empty in list mode, got: %q", stdout) 135 + } 136 + } 137 + 138 + func TestTestValid(t *testing.T) { 139 + stdout, _, err := run(t, []string{"-t", "hello.txt.gz"}, nil, newFS(t)) 140 + if err != nil { 141 + t.Fatalf("unexpected error: %v", err) 142 + } 143 + if len(stdout) > 0 { 144 + t.Errorf("stdout should be empty in test mode, got: %q", stdout) 145 + } 146 + } 147 + 148 + func TestTestInvalid(t *testing.T) { 149 + _, _, err := run(t, []string{"-t", "notgz.txt"}, nil, newFS(t)) 150 + if err == nil { 151 + t.Fatal("expected error for non-gzip file") 152 + } 153 + } 154 + 155 + func TestVerbose(t *testing.T) { 156 + stdout, stderr, err := run(t, []string{"-v", "hello.txt.gz"}, nil, newFS(t)) 157 + if err != nil { 158 + t.Fatalf("unexpected error: %v", err) 159 + } 160 + stderrStr := string(stderr) 161 + if !contains(stderrStr, "hello.txt.gz") || !contains(stderrStr, "OK") { 162 + t.Errorf("stderr should contain verbose info: %q", stderrStr) 163 + } 164 + if want := "hello world"; string(stdout) != want { 165 + t.Errorf("stdout = %q, want %q", stdout, want) 166 + } 167 + } 168 + 169 + func TestEmptyGzipStdin(t *testing.T) { 170 + stdout, stderr, err := run(t, nil, gzipData(t, []byte{}), newFS(t)) 171 + if err != nil { 172 + t.Fatalf("unexpected error: %v", err) 173 + } 174 + if len(stderr) > 0 { 175 + t.Fatalf("unexpected stderr: %q", stderr) 176 + } 177 + if len(stdout) != 0 { 178 + t.Errorf("stdout should be empty, got: %q", stdout) 179 + } 180 + } 181 + 182 + func TestMissingFile(t *testing.T) { 183 + _, stderr, err := run(t, []string{"nope.gz"}, nil, newFS(t)) 184 + if err == nil { 185 + t.Fatal("expected error for missing file") 186 + } 187 + if !contains(string(stderr), "No such file or directory") { 188 + t.Errorf("stderr should mention 'No such file or directory': %q", stderr) 189 + } 190 + } 191 + 192 + func TestNotGzip(t *testing.T) { 193 + _, stderr, err := run(t, []string{"notgz.txt"}, nil, newFS(t)) 194 + if err == nil { 195 + t.Fatal("expected error for non-gzip file") 196 + } 197 + if !contains(string(stderr), "not in gzip format") { 198 + t.Errorf("stderr should mention 'not in gzip format': %q", stderr) 199 + } 200 + } 201 + 202 + func TestHelp(t *testing.T) { 203 + _, stderr, err := run(t, []string{"--help"}, nil, newFS(t)) 204 + if err != nil { 205 + t.Fatalf("unexpected error: %v", err) 206 + } 207 + stderrStr := string(stderr) 208 + if !contains(stderrStr, "Usage: zcat") { 209 + t.Errorf("stderr should contain usage: %q", stderrStr) 210 + } 211 + if !contains(stderrStr, "--force") { 212 + t.Errorf("stderr should contain --help flag info: %q", stderrStr) 213 + } 214 + } 215 + 216 + func TestUnknownFlag(t *testing.T) { 217 + _, _, err := run(t, []string{"--nope"}, nil, newFS(t)) 218 + if err == nil { 219 + t.Fatal("expected error for unknown flag") 220 + } 221 + } 222 + 223 + func TestConcatenatedRoundTrip(t *testing.T) { 224 + foo := gzipData(t, []byte("foo")) 225 + bar := gzipData(t, []byte("bar")) 226 + combined := append(foo, bar...) 227 + 228 + stdout, stderr, err := run(t, nil, combined, newFS(t)) 229 + if err != nil { 230 + t.Fatalf("unexpected error: %v", err) 231 + } 232 + if len(stderr) > 0 { 233 + t.Fatalf("unexpected stderr: %q", stderr) 234 + } 235 + 236 + if want := "foo"; string(stdout[:3]) != want { 237 + t.Errorf("first part stdout = %q, want %q", stdout[:3], want) 238 + } 239 + if want := "bar"; string(stdout[3:]) != want { 240 + t.Errorf("second part stdout = %q, want %q", stdout[3:], want) 241 + } 242 + } 243 + 244 + func contains(s, substr string) bool { 245 + return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || findInString(s, substr))) 246 + } 247 + 248 + func findInString(s, substr string) bool { 249 + for i := 0; i <= len(s)-len(substr); i++ { 250 + if s[i:i+len(substr)] == substr { 251 + return true 252 + } 253 + } 254 + return false 255 + }
+8
command/registry/coreutils/coreutils.go
··· 17 17 "tangled.org/xeiaso.net/kefka/command/internal/file" 18 18 "tangled.org/xeiaso.net/kefka/command/internal/falsecmd" 19 19 "tangled.org/xeiaso.net/kefka/command/internal/fold" 20 + "tangled.org/xeiaso.net/kefka/command/internal/gunzip" 21 + "tangled.org/xeiaso.net/kefka/command/internal/gzip" 20 22 "tangled.org/xeiaso.net/kefka/command/internal/hostname" 23 + "tangled.org/xeiaso.net/kefka/command/internal/join" 21 24 "tangled.org/xeiaso.net/kefka/command/internal/ls" 22 25 "tangled.org/xeiaso.net/kefka/command/internal/truecmd" 23 26 "tangled.org/xeiaso.net/kefka/command/internal/unexpand" 27 + "tangled.org/xeiaso.net/kefka/command/internal/zcat" 24 28 "tangled.org/xeiaso.net/kefka/command/registry" 25 29 ) 26 30 ··· 41 45 reg.Register("file", file.Impl{}) 42 46 reg.Register("false", falsecmd.Impl{}) 43 47 reg.Register("fold", fold.Impl{}) 48 + reg.Register("gunzip", gunzip.Impl{}) 49 + reg.Register("gzip", gzip.Impl{}) 44 50 reg.Register("hostname", hostname.Impl{}) 51 + reg.Register("join", join.Impl{}) 45 52 reg.Register("ls", ls.Impl{}) 46 53 reg.Register("true", truecmd.Impl{}) 47 54 reg.Register("unexpand", unexpand.Impl{}) 55 + reg.Register("zcat", zcat.Impl{}) 48 56 }