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

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

Xe Iaso 4cda6738 4d474c84

+377
+143
command/internal/readlink/readlink.go
··· 1 + package readlink 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "fmt" 7 + "io" 8 + "path" 9 + "strings" 10 + 11 + "github.com/go-git/go-billy/v5" 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(ctx context.Context, ec *command.ExecContext, args []string) error { 20 + if ec == nil { 21 + return errors.New("readlink: nil ExecContext") 22 + } 23 + if ec.FS == nil { 24 + return errors.New("readlink: ExecContext has no filesystem") 25 + } 26 + 27 + stdout := ec.Stdout 28 + if stdout == nil { 29 + stdout = io.Discard 30 + } 31 + stderr := ec.Stderr 32 + if stderr == nil { 33 + stderr = io.Discard 34 + } 35 + 36 + set := getopt.New() 37 + set.SetProgram("readlink") 38 + set.SetParameters("FILE...") 39 + 40 + usage := func() { 41 + fmt.Fprint(stderr, "Usage: readlink [OPTIONS] FILE...\n") 42 + fmt.Fprint(stderr, "Print resolved symbolic links or canonical file names.\n\n") 43 + fmt.Fprint(stderr, " -f, --canonicalize canonicalize by following every symlink in every component of the given name recursively\n") 44 + fmt.Fprint(stderr, " --help display this help and exit\n") 45 + } 46 + set.SetUsage(usage) 47 + 48 + canonicalize := set.BoolLong("canonicalize", 'f', "canonicalize by following every symlink in every component of the given name recursively") 49 + help := set.BoolLong("help", 0, "display this help and exit") 50 + 51 + if err := set.Getopt(append([]string{"readlink"}, args...), nil); err != nil { 52 + fmt.Fprintf(stderr, "readlink: %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 + if len(files) == 0 { 64 + fmt.Fprint(stderr, "readlink: missing operand\n") 65 + return interp.ExitStatus(1) 66 + } 67 + 68 + sym, hasSymlink := ec.FS.(billy.Symlink) 69 + 70 + anyError := false 71 + for _, file := range files { 72 + filePath := resolvePath(ec, file) 73 + 74 + if *canonicalize { 75 + current := filePath 76 + seen := make(map[string]struct{}) 77 + for { 78 + if _, ok := seen[current]; ok { 79 + break 80 + } 81 + seen[current] = struct{}{} 82 + 83 + if !hasSymlink { 84 + break 85 + } 86 + target, err := sym.Readlink(current) 87 + if err != nil { 88 + break 89 + } 90 + if path.IsAbs(target) { 91 + trimmed := strings.TrimPrefix(target, "/") 92 + if trimmed == "" { 93 + current = "." 94 + } else { 95 + current = path.Clean(trimmed) 96 + } 97 + } else { 98 + dir := path.Dir(current) 99 + current = path.Join(dir, target) 100 + } 101 + } 102 + io.WriteString(stdout, current) 103 + io.WriteString(stdout, "\n") 104 + continue 105 + } 106 + 107 + if !hasSymlink { 108 + anyError = true 109 + continue 110 + } 111 + target, err := sym.Readlink(filePath) 112 + if err != nil { 113 + anyError = true 114 + continue 115 + } 116 + io.WriteString(stdout, target) 117 + io.WriteString(stdout, "\n") 118 + } 119 + 120 + if anyError { 121 + return interp.ExitStatus(1) 122 + } 123 + return nil 124 + } 125 + 126 + func resolvePath(ec *command.ExecContext, p string) string { 127 + dir := ec.Dir 128 + if dir == "" { 129 + dir = "." 130 + } 131 + if path.IsAbs(p) { 132 + p = strings.TrimPrefix(p, "/") 133 + if p == "" { 134 + return "." 135 + } 136 + return path.Clean(p) 137 + } 138 + joined := path.Join(dir, p) 139 + if joined == "" { 140 + return "." 141 + } 142 + return joined 143 + }