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

Strip the last component from each NAME. Mirrors GNU dirname
semantics as implemented by just-bash: trailing slashes are
collapsed before splitting, a name with no slash yields ".",
and a name whose only slashes are leading yields "/".

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

Xe Iaso 3984d634 dce3bcc4

+218
+82
command/internal/dirname/dirname.go
··· 1 + package dirname 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("dirname: 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("dirname") 33 + set.SetParameters("NAME...") 34 + 35 + usage := func() { 36 + fmt.Fprint(stderr, "Usage: dirname [OPTION] NAME...\n") 37 + fmt.Fprint(stderr, "Strip last component from file name.\n\n") 38 + fmt.Fprint(stderr, " --help display this help and exit\n") 39 + } 40 + set.SetUsage(usage) 41 + 42 + help := set.BoolLong("help", 0, "display this help and exit") 43 + 44 + if err := set.Getopt(append([]string{"dirname"}, args...), nil); err != nil { 45 + fmt.Fprintf(stderr, "dirname: %s\n", err) 46 + usage() 47 + return interp.ExitStatus(1) 48 + } 49 + 50 + if *help { 51 + usage() 52 + return nil 53 + } 54 + 55 + names := set.Args() 56 + if len(names) == 0 { 57 + fmt.Fprint(stderr, "dirname: missing operand\n") 58 + return interp.ExitStatus(1) 59 + } 60 + 61 + results := make([]string, 0, len(names)) 62 + for _, name := range names { 63 + results = append(results, dirnameOf(name)) 64 + } 65 + 66 + io.WriteString(stdout, strings.Join(results, "\n")) 67 + io.WriteString(stdout, "\n") 68 + return nil 69 + } 70 + 71 + func dirnameOf(name string) string { 72 + clean := strings.TrimRight(name, "/") 73 + idx := strings.LastIndex(clean, "/") 74 + switch idx { 75 + case -1: 76 + return "." 77 + case 0: 78 + return "/" 79 + default: 80 + return clean[:idx] 81 + } 82 + }
+134
command/internal/dirname/dirname_test.go
··· 1 + package dirname 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 TestDirname(t *testing.T) { 25 + tests := []struct { 26 + name string 27 + args []string 28 + wantStdout string 29 + wantErrSub string 30 + wantErr bool 31 + }{ 32 + { 33 + name: "single path", 34 + args: []string{"/usr/local/bin/sh"}, 35 + wantStdout: "/usr/local/bin\n", 36 + }, 37 + { 38 + name: "no slash", 39 + args: []string{"foo"}, 40 + wantStdout: ".\n", 41 + }, 42 + { 43 + name: "trailing slash", 44 + args: []string{"/usr/local/bin/"}, 45 + wantStdout: "/usr/local\n", 46 + }, 47 + { 48 + name: "multiple trailing slashes", 49 + args: []string{"foo/bar///"}, 50 + wantStdout: "foo\n", 51 + }, 52 + { 53 + name: "root only", 54 + args: []string{"/"}, 55 + wantStdout: ".\n", 56 + }, 57 + { 58 + name: "single component under root", 59 + args: []string{"/foo"}, 60 + wantStdout: "/\n", 61 + }, 62 + { 63 + name: "single component under root with trailing slash", 64 + args: []string{"/foo/"}, 65 + wantStdout: "/\n", 66 + }, 67 + { 68 + name: "empty string", 69 + args: []string{""}, 70 + wantStdout: ".\n", 71 + }, 72 + { 73 + name: "relative two-component path", 74 + args: []string{"foo/bar"}, 75 + wantStdout: "foo\n", 76 + }, 77 + { 78 + name: "multiple operands", 79 + args: []string{"/a/b", "/c/d", "e"}, 80 + wantStdout: "/a\n/c\n.\n", 81 + }, 82 + { 83 + name: "missing operand", 84 + args: []string{}, 85 + wantErrSub: "missing operand", 86 + wantErr: true, 87 + }, 88 + { 89 + name: "unknown flag", 90 + args: []string{"--no-such-flag", "foo"}, 91 + wantErr: true, 92 + }, 93 + { 94 + name: "double dash terminator", 95 + args: []string{"--", "-weird-name"}, 96 + wantStdout: ".\n", 97 + }, 98 + } 99 + 100 + for _, tt := range tests { 101 + t.Run(tt.name, func(t *testing.T) { 102 + stdout, stderr, err := run(t, tt.args) 103 + if tt.wantErr { 104 + if err == nil { 105 + t.Fatalf("expected error, got nil; stdout=%q stderr=%q", stdout, stderr) 106 + } 107 + } else if err != nil { 108 + t.Fatalf("unexpected error: %v; stderr=%q", err, stderr) 109 + } 110 + if tt.wantStdout != "" && stdout != tt.wantStdout { 111 + t.Errorf("stdout = %q, want %q", stdout, tt.wantStdout) 112 + } 113 + if tt.wantErrSub != "" && !strings.Contains(stderr, tt.wantErrSub) { 114 + t.Errorf("stderr = %q, want substring %q", stderr, tt.wantErrSub) 115 + } 116 + }) 117 + } 118 + } 119 + 120 + func TestHelp(t *testing.T) { 121 + stdout, stderr, err := run(t, []string{"--help"}) 122 + if err != nil { 123 + t.Fatalf("unexpected error: %v", err) 124 + } 125 + if stdout != "" { 126 + t.Errorf("expected empty stdout, got %q", stdout) 127 + } 128 + if !strings.Contains(stderr, "Usage: dirname [OPTION] NAME...") { 129 + t.Errorf("usage line missing from stderr: %q", stderr) 130 + } 131 + if !strings.Contains(stderr, "--help") { 132 + t.Errorf("help flag missing from help output: %q", stderr) 133 + } 134 + }
+2
command/registry/coreutils/coreutils.go
··· 10 10 "tangled.org/xeiaso.net/kefka/command/internal/cut" 11 11 "tangled.org/xeiaso.net/kefka/command/internal/date" 12 12 "tangled.org/xeiaso.net/kefka/command/internal/diff" 13 + "tangled.org/xeiaso.net/kefka/command/internal/dirname" 13 14 "tangled.org/xeiaso.net/kefka/command/internal/falsecmd" 14 15 "tangled.org/xeiaso.net/kefka/command/internal/hostname" 15 16 "tangled.org/xeiaso.net/kefka/command/internal/ls" ··· 27 28 reg.Register("cut", cut.Impl{}) 28 29 reg.Register("date", date.Impl{}) 29 30 reg.Register("diff", diff.Impl{}) 31 + reg.Register("dirname", dirname.Impl{}) 30 32 reg.Register("false", falsecmd.Impl{}) 31 33 reg.Register("hostname", hostname.Impl{}) 32 34 reg.Register("ls", ls.Impl{})