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

Strip directory and suffix from filenames. Mirrors GNU basename
semantics as implemented by just-bash, including the suffix-implies-
multiple shorthand and empty output when the suffix equals the base.

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

Xe Iaso fa829dce 926863cd

+253
+94
command/internal/basename/basename.go
··· 1 + package basename 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "fmt" 7 + "io" 8 + "strings" 9 + 10 + "github.com/pborman/getopt/v2" 11 + "mvdan.cc/sh/v3/interp" 12 + "tangled.org/xeiaso.net/kefka/command" 13 + ) 14 + 15 + type Impl struct{} 16 + 17 + func (Impl) Exec(ctx context.Context, ec *command.ExecContext, args []string) error { 18 + if ec == nil { 19 + return errors.New("basename: nil ExecContext") 20 + } 21 + 22 + stdout := ec.Stdout 23 + if stdout == nil { 24 + stdout = io.Discard 25 + } 26 + stderr := ec.Stderr 27 + if stderr == nil { 28 + stderr = io.Discard 29 + } 30 + 31 + set := getopt.New() 32 + set.SetProgram("basename") 33 + set.SetParameters("NAME [SUFFIX]") 34 + 35 + usage := func() { 36 + fmt.Fprint(stderr, "Usage: basename NAME [SUFFIX]\n") 37 + fmt.Fprint(stderr, " or: basename OPTION... NAME...\n") 38 + fmt.Fprint(stderr, "Strip directory and suffix from filenames.\n\n") 39 + fmt.Fprint(stderr, " -a, --multiple support multiple arguments\n") 40 + fmt.Fprint(stderr, " -s, --suffix=SUFFIX remove a trailing SUFFIX\n") 41 + fmt.Fprint(stderr, " --help display this help and exit\n") 42 + } 43 + set.SetUsage(usage) 44 + 45 + multiple := set.BoolLong("multiple", 'a', "support multiple arguments") 46 + suffix := set.StringLong("suffix", 's', "", "remove a trailing SUFFIX") 47 + help := set.BoolLong("help", 0, "display this help and exit") 48 + 49 + if err := set.Getopt(append([]string{"basename"}, args...), nil); err != nil { 50 + fmt.Fprintf(stderr, "basename: %s\n", err) 51 + usage() 52 + return interp.ExitStatus(1) 53 + } 54 + 55 + if *help { 56 + usage() 57 + return nil 58 + } 59 + 60 + if set.Lookup("suffix").Seen() { 61 + *multiple = true 62 + } 63 + 64 + names := set.Args() 65 + if len(names) == 0 { 66 + fmt.Fprint(stderr, "basename: missing operand\n") 67 + return interp.ExitStatus(1) 68 + } 69 + 70 + suf := *suffix 71 + if !*multiple && len(names) >= 2 { 72 + suf = names[len(names)-1] 73 + names = names[:len(names)-1] 74 + } 75 + 76 + results := make([]string, 0, len(names)) 77 + for _, name := range names { 78 + results = append(results, basenameOf(name, suf)) 79 + } 80 + 81 + io.WriteString(stdout, strings.Join(results, "\n")) 82 + io.WriteString(stdout, "\n") 83 + return nil 84 + } 85 + 86 + func basenameOf(name, suffix string) string { 87 + clean := strings.TrimRight(name, "/") 88 + idx := strings.LastIndex(clean, "/") 89 + base := clean[idx+1:] 90 + if suffix != "" && strings.HasSuffix(base, suffix) { 91 + base = base[:len(base)-len(suffix)] 92 + } 93 + return base 94 + }
+157
command/internal/basename/basename_test.go
··· 1 + package basename 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "strings" 7 + "testing" 8 + 9 + "tangled.org/xeiaso.net/kefka/command" 10 + ) 11 + 12 + func run(t *testing.T, args []string) (string, string, error) { 13 + t.Helper() 14 + var stdout, stderr bytes.Buffer 15 + ec := &command.ExecContext{ 16 + Stdout: &stdout, 17 + Stderr: &stderr, 18 + Dir: ".", 19 + } 20 + err := Impl{}.Exec(context.Background(), ec, args) 21 + return stdout.String(), stderr.String(), err 22 + } 23 + 24 + func TestBasename(t *testing.T) { 25 + tests := []struct { 26 + name string 27 + args []string 28 + wantStdout string 29 + wantErrSub string // substring expected in stderr (when set) 30 + wantErr bool 31 + }{ 32 + { 33 + name: "single path", 34 + args: []string{"/usr/local/bin/sh"}, 35 + wantStdout: "sh\n", 36 + }, 37 + { 38 + name: "no slash", 39 + args: []string{"foo"}, 40 + wantStdout: "foo\n", 41 + }, 42 + { 43 + name: "trailing slash", 44 + args: []string{"/usr/local/bin/"}, 45 + wantStdout: "bin\n", 46 + }, 47 + { 48 + name: "multiple trailing slashes", 49 + args: []string{"foo/bar///"}, 50 + wantStdout: "bar\n", 51 + }, 52 + { 53 + name: "root only", 54 + args: []string{"/"}, 55 + wantStdout: "\n", 56 + }, 57 + { 58 + name: "empty string", 59 + args: []string{""}, 60 + wantStdout: "\n", 61 + }, 62 + { 63 + name: "second positional treated as suffix", 64 + args: []string{"foo.txt", ".txt"}, 65 + wantStdout: "foo\n", 66 + }, 67 + { 68 + name: "suffix only stripped at end", 69 + args: []string{"a.txt.bak", ".txt"}, 70 + wantStdout: "a.txt.bak\n", 71 + }, 72 + { 73 + name: "short suffix flag", 74 + args: []string{"-s", ".txt", "foo.txt", "bar.txt"}, 75 + wantStdout: "foo\nbar\n", 76 + }, 77 + { 78 + name: "long suffix flag with equals", 79 + args: []string{"--suffix=.txt", "foo.txt", "bar.txt"}, 80 + wantStdout: "foo\nbar\n", 81 + }, 82 + { 83 + name: "multiple flag short", 84 + args: []string{"-a", "foo.txt", "bar.txt"}, 85 + wantStdout: "foo.txt\nbar.txt\n", 86 + }, 87 + { 88 + name: "multiple flag long", 89 + args: []string{"--multiple", "/a/b", "/c/d"}, 90 + wantStdout: "b\nd\n", 91 + }, 92 + { 93 + name: "suffix implies multiple", 94 + args: []string{"-s", ".log", "/var/log/a.log", "/var/log/b.log"}, 95 + wantStdout: "a\nb\n", 96 + }, 97 + { 98 + name: "missing operand", 99 + args: []string{}, 100 + wantErrSub: "missing operand", 101 + wantErr: true, 102 + }, 103 + { 104 + name: "unknown flag", 105 + args: []string{"--no-such-flag", "foo"}, 106 + wantErr: true, 107 + }, 108 + { 109 + name: "suffix equals base produces empty", 110 + args: []string{"-a", "-s", ".txt", ".txt"}, 111 + wantStdout: "\n", 112 + }, 113 + { 114 + name: "double dash terminator", 115 + args: []string{"--", "-weird-name"}, 116 + wantStdout: "-weird-name\n", 117 + }, 118 + } 119 + 120 + for _, tt := range tests { 121 + t.Run(tt.name, func(t *testing.T) { 122 + stdout, stderr, err := run(t, tt.args) 123 + if tt.wantErr { 124 + if err == nil { 125 + t.Fatalf("expected error, got nil; stdout=%q stderr=%q", stdout, stderr) 126 + } 127 + } else if err != nil { 128 + t.Fatalf("unexpected error: %v; stderr=%q", err, stderr) 129 + } 130 + if tt.wantStdout != "" && stdout != tt.wantStdout { 131 + t.Errorf("stdout = %q, want %q", stdout, tt.wantStdout) 132 + } 133 + if tt.wantErrSub != "" && !strings.Contains(stderr, tt.wantErrSub) { 134 + t.Errorf("stderr = %q, want substring %q", stderr, tt.wantErrSub) 135 + } 136 + }) 137 + } 138 + } 139 + 140 + func TestHelp(t *testing.T) { 141 + stdout, stderr, err := run(t, []string{"--help"}) 142 + if err != nil { 143 + t.Fatalf("unexpected error: %v", err) 144 + } 145 + if stdout != "" { 146 + t.Errorf("expected empty stdout, got %q", stdout) 147 + } 148 + if !strings.Contains(stderr, "Usage: basename NAME [SUFFIX]") { 149 + t.Errorf("usage line missing from stderr: %q", stderr) 150 + } 151 + if !strings.Contains(stderr, "-a, --multiple") { 152 + t.Errorf("multiple flag missing from help: %q", stderr) 153 + } 154 + if !strings.Contains(stderr, "-s, --suffix=SUFFIX") { 155 + t.Errorf("suffix flag missing from help: %q", stderr) 156 + } 157 + }
+2
command/registry/coreutils/coreutils.go
··· 2 2 3 3 import ( 4 4 "tangled.org/xeiaso.net/kefka/command/internal/base64" 5 + "tangled.org/xeiaso.net/kefka/command/internal/basename" 5 6 "tangled.org/xeiaso.net/kefka/command/internal/falsecmd" 6 7 "tangled.org/xeiaso.net/kefka/command/internal/hostname" 7 8 "tangled.org/xeiaso.net/kefka/command/internal/ls" ··· 11 12 12 13 func Register(reg *registry.Impl) { 13 14 reg.Register("base64", base64.Impl{}) 15 + reg.Register("basename", basename.Impl{}) 14 16 reg.Register("false", falsecmd.Impl{}) 15 17 reg.Register("hostname", hostname.Impl{}) 16 18 reg.Register("ls", ls.Impl{})